From: Eric Harney Date: Wed, 6 Feb 2013 15:26:45 +0000 (-0500) Subject: Add GlusterFS volume driver X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=06b26a8ab82fa4584f8bc0a217296f7b7510d0a4;p=openstack-build%2Fcinder-build.git Add GlusterFS volume driver This driver enables use of GlusterFS in a similar fashion as the NFS driver. It supports basic volume operations, and like NFS, does not support snapshot/clone. To enable, set volume_driver to cinder.volume.drivers.glusterfs.GlusterfsDriver Note that this requires a Nova libvirt GlusterFS driver as well. Adds config options: glusterfs_shares_config, glusterfs_mount_point_base, glusterfs_disk_util, and glusterfs_sparsed_volumes. DocImpact Change-Id: I3dd4018f0cb4db48348728ca66bae7918309bb32 --- diff --git a/cinder/exception.py b/cinder/exception.py index 39ac7098e..14e1ba4b7 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -522,6 +522,18 @@ class NfsNoSuitableShareFound(NotFound): message = _("There is no share which can host %(volume_size)sG") +class GlusterfsException(CinderException): + message = _("Unknown Gluster exception") + + +class GlusterfsNoSharesMounted(NotFound): + message = _("No mounted Gluster shares found") + + +class GlusterfsNoSuitableShareFound(NotFound): + message = _("There is no share which can host %(volume_size)sG") + + class GlanceMetadataExists(Invalid): message = _("Glance metadata cannot be updated, key %(key)s" " exists for volume id %(volume_id)s") diff --git a/cinder/tests/test_glusterfs.py b/cinder/tests/test_glusterfs.py new file mode 100644 index 000000000..191b4eb1f --- /dev/null +++ b/cinder/tests/test_glusterfs.py @@ -0,0 +1,653 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 GlusterFS driver module.""" + +import __builtin__ +import errno +import os + +import mox as mox_lib +from mox import IgnoreArg +from mox import IsA +from mox import stubout + +from cinder import context +from cinder import exception +from cinder.exception import ProcessExecutionError +from cinder import test + +from cinder.volume.drivers import glusterfs + + +class DumbVolume(object): + fields = {} + + def __setitem__(self, key, value): + self.fields[key] = value + + def __getitem__(self, item): + return self.fields[item] + + +class GlusterFsDriverTestCase(test.TestCase): + """Test case for GlusterFS driver.""" + + TEST_EXPORT1 = 'glusterfs-host1:/export' + TEST_EXPORT2 = 'glusterfs-host2:/export' + TEST_SIZE_IN_GB = 1 + TEST_MNT_POINT = '/mnt/glusterfs' + TEST_MNT_POINT_BASE = '/mnt/test' + TEST_LOCAL_PATH = '/mnt/glusterfs/volume-123' + TEST_FILE_NAME = 'test.txt' + TEST_SHARES_CONFIG_FILE = '/etc/cinder/test-shares.conf' + ONE_GB_IN_BYTES = 1024 * 1024 * 1024 + + def setUp(self): + self._driver = glusterfs.GlusterfsDriver() + self._mox = mox_lib.Mox() + self.stubs = stubout.StubOutForTesting() + + def tearDown(self): + self._mox.UnsetStubs() + self.stubs.UnsetAll() + + def stub_out_not_replaying(self, obj, attr_name): + attr_to_replace = getattr(obj, attr_name) + stub = mox_lib.MockObject(attr_to_replace) + self.stubs.Set(obj, attr_name, stub) + + def test_path_exists_should_return_true(self): + """_path_exists should return True if stat returns 0.""" + mox = self._mox + drv = self._driver + + mox.StubOutWithMock(drv, '_execute') + drv._execute('stat', self.TEST_FILE_NAME, run_as_root=True) + + mox.ReplayAll() + + self.assertTrue(drv._path_exists(self.TEST_FILE_NAME)) + + mox.VerifyAll() + + def test_path_exists_should_return_false(self): + """_path_exists should return True if stat doesn't return 0.""" + mox = self._mox + drv = self._driver + + mox.StubOutWithMock(drv, '_execute') + drv._execute( + 'stat', + self.TEST_FILE_NAME, run_as_root=True).\ + AndRaise(ProcessExecutionError( + stderr="stat: cannot stat `test.txt': No such file " + "or directory")) + + mox.ReplayAll() + + self.assertFalse(drv._path_exists(self.TEST_FILE_NAME)) + + mox.VerifyAll() + + def test_local_path(self): + """local_path common use case.""" + glusterfs.FLAGS.glusterfs_mount_point_base = self.TEST_MNT_POINT_BASE + drv = self._driver + + volume = DumbVolume() + volume['provider_location'] = self.TEST_EXPORT1 + volume['name'] = 'volume-123' + + self.assertEqual( + '/mnt/test/ab03ab34eaca46a5fb81878f7e9b91fc/volume-123', + drv.local_path(volume)) + + def test_mount_glusterfs_should_mount_correctly(self): + """_mount_glusterfs common case usage.""" + mox = self._mox + drv = self._driver + + mox.StubOutWithMock(drv, '_path_exists') + drv._path_exists(self.TEST_MNT_POINT).AndReturn(True) + + mox.StubOutWithMock(drv, '_execute') + drv._execute('mount', '-t', 'glusterfs', self.TEST_EXPORT1, + self.TEST_MNT_POINT, run_as_root=True) + + mox.ReplayAll() + + drv._mount_glusterfs(self.TEST_EXPORT1, self.TEST_MNT_POINT) + + mox.VerifyAll() + + def test_mount_glusterfs_should_suppress_already_mounted_error(self): + """_mount_glusterfs should suppress already mounted error if + ensure=True + """ + mox = self._mox + drv = self._driver + + mox.StubOutWithMock(drv, '_path_exists') + drv._path_exists(self.TEST_MNT_POINT).AndReturn(True) + + mox.StubOutWithMock(drv, '_execute') + drv._execute('mount', '-t', 'glusterfs', self.TEST_EXPORT1, + self.TEST_MNT_POINT, run_as_root=True).\ + AndRaise(ProcessExecutionError( + stderr='is busy or already mounted')) + + mox.ReplayAll() + + drv._mount_glusterfs(self.TEST_EXPORT1, self.TEST_MNT_POINT, + ensure=True) + + mox.VerifyAll() + + def test_mount_glusterfs_should_reraise_already_mounted_error(self): + """_mount_glusterfs should not suppress already mounted error + if ensure=False + """ + mox = self._mox + drv = self._driver + + mox.StubOutWithMock(drv, '_path_exists') + drv._path_exists(self.TEST_MNT_POINT).AndReturn(True) + + mox.StubOutWithMock(drv, '_execute') + drv._execute( + 'mount', + '-t', + 'glusterfs', + self.TEST_EXPORT1, + self.TEST_MNT_POINT, + run_as_root=True). \ + AndRaise(ProcessExecutionError(stderr='is busy or ' + 'already mounted')) + + mox.ReplayAll() + + self.assertRaises(ProcessExecutionError, drv._mount_glusterfs, + self.TEST_EXPORT1, self.TEST_MNT_POINT, + ensure=False) + + mox.VerifyAll() + + def test_mount_glusterfs_should_create_mountpoint_if_not_yet(self): + """_mount_glusterfs should create mountpoint if it doesn't exist.""" + mox = self._mox + drv = self._driver + + mox.StubOutWithMock(drv, '_path_exists') + drv._path_exists(self.TEST_MNT_POINT).AndReturn(False) + + mox.StubOutWithMock(drv, '_execute') + drv._execute('mkdir', '-p', self.TEST_MNT_POINT) + drv._execute(*([IgnoreArg()] * 5), run_as_root=IgnoreArg()) + + mox.ReplayAll() + + drv._mount_glusterfs(self.TEST_EXPORT1, self.TEST_MNT_POINT) + + mox.VerifyAll() + + def test_mount_glusterfs_should_not_create_mountpoint_if_already(self): + """_mount_glusterfs should not create mountpoint if it already exists. + """ + mox = self._mox + drv = self._driver + + mox.StubOutWithMock(drv, '_path_exists') + drv._path_exists(self.TEST_MNT_POINT).AndReturn(True) + + mox.StubOutWithMock(drv, '_execute') + drv._execute(*([IgnoreArg()] * 5), run_as_root=IgnoreArg()) + + mox.ReplayAll() + + drv._mount_glusterfs(self.TEST_EXPORT1, self.TEST_MNT_POINT) + + mox.VerifyAll() + + def test_get_hash_str(self): + """_get_hash_str should calculation correct value.""" + drv = self._driver + + self.assertEqual('ab03ab34eaca46a5fb81878f7e9b91fc', + drv._get_hash_str(self.TEST_EXPORT1)) + + def test_get_mount_point_for_share(self): + """_get_mount_point_for_share should calculate correct value.""" + drv = self._driver + + glusterfs.FLAGS.glusterfs_mount_point_base = self.TEST_MNT_POINT_BASE + + self.assertEqual('/mnt/test/ab03ab34eaca46a5fb81878f7e9b91fc', + drv._get_mount_point_for_share( + self.TEST_EXPORT1)) + + def test_get_available_capacity_with_df(self): + """_get_available_capacity should calculate correct value.""" + mox = self._mox + drv = self._driver + + df_avail = 1490560 + df_head = 'Filesystem 1K-blocks Used Available Use% Mounted on\n' + df_data = 'glusterfs-host:/export 2620544 996864 %d 41%% /mnt' % \ + df_avail + df_output = df_head + df_data + + setattr(glusterfs.FLAGS, 'glusterfs_disk_util', 'df') + + mox.StubOutWithMock(drv, '_get_mount_point_for_share') + drv._get_mount_point_for_share(self.TEST_EXPORT1).\ + AndReturn(self.TEST_MNT_POINT) + + mox.StubOutWithMock(drv, '_execute') + drv._execute('df', '--portability', '--block-size', '1', + self.TEST_MNT_POINT, + run_as_root=True).AndReturn((df_output, None)) + + mox.ReplayAll() + + self.assertEquals(df_avail, + drv._get_available_capacity( + self.TEST_EXPORT1)) + + mox.VerifyAll() + + delattr(glusterfs.FLAGS, 'glusterfs_disk_util') + + def test_get_available_capacity_with_du(self): + """_get_available_capacity should calculate correct value.""" + mox = self._mox + drv = self._driver + + setattr(glusterfs.FLAGS, 'glusterfs_disk_util', 'du') + + df_total_size = 2620544 + df_used_size = 996864 + df_avail_size = 1490560 + df_title = 'Filesystem 1-blocks Used Available Use% Mounted on\n' + df_mnt_data = 'glusterfs-host:/export %d %d %d 41%% /mnt' % \ + (df_total_size, + df_used_size, + df_avail_size) + df_output = df_title + df_mnt_data + + du_used = 490560 + du_output = '%d /mnt' % du_used + + mox.StubOutWithMock(drv, '_get_mount_point_for_share') + drv._get_mount_point_for_share(self.TEST_EXPORT1).\ + AndReturn(self.TEST_MNT_POINT) + + mox.StubOutWithMock(drv, '_execute') + drv._execute('df', '--portability', '--block-size', '1', + self.TEST_MNT_POINT, + run_as_root=True).\ + AndReturn((df_output, None)) + drv._execute('du', '-sb', '--apparent-size', + '--exclude', '*snapshot*', + self.TEST_MNT_POINT, + run_as_root=True).AndReturn((du_output, None)) + + mox.ReplayAll() + + self.assertEquals(df_total_size - du_used, + drv._get_available_capacity( + self.TEST_EXPORT1)) + + mox.VerifyAll() + + delattr(glusterfs.FLAGS, 'glusterfs_disk_util') + + def test_load_shares_config(self): + mox = self._mox + drv = self._driver + + glusterfs.FLAGS.glusterfs_shares_config = self.TEST_SHARES_CONFIG_FILE + + mox.StubOutWithMock(__builtin__, 'open') + config_data = [] + config_data.append(self.TEST_EXPORT1) + config_data.append('#' + self.TEST_EXPORT2) + config_data.append('') + __builtin__.open(self.TEST_SHARES_CONFIG_FILE).AndReturn(config_data) + mox.ReplayAll() + + shares = drv._load_shares_config() + + self.assertEqual([self.TEST_EXPORT1], shares) + + mox.VerifyAll() + + def test_ensure_share_mounted(self): + """_ensure_share_mounted simple use case.""" + mox = self._mox + drv = self._driver + + mox.StubOutWithMock(drv, '_get_mount_point_for_share') + drv._get_mount_point_for_share(self.TEST_EXPORT1).\ + AndReturn(self.TEST_MNT_POINT) + + mox.StubOutWithMock(drv, '_mount_glusterfs') + drv._mount_glusterfs(self.TEST_EXPORT1, self.TEST_MNT_POINT, + ensure=True) + + mox.ReplayAll() + + drv._ensure_share_mounted(self.TEST_EXPORT1) + + mox.VerifyAll() + + def test_ensure_shares_mounted_should_save_mounting_successfully(self): + """_ensure_shares_mounted should save share if mounted with success.""" + mox = self._mox + drv = self._driver + + mox.StubOutWithMock(drv, '_load_shares_config') + drv._load_shares_config().AndReturn([self.TEST_EXPORT1]) + mox.StubOutWithMock(drv, '_ensure_share_mounted') + drv._ensure_share_mounted(self.TEST_EXPORT1) + + mox.ReplayAll() + + drv._ensure_shares_mounted() + + self.assertEqual(1, len(drv._mounted_shares)) + self.assertEqual(self.TEST_EXPORT1, drv._mounted_shares[0]) + + mox.VerifyAll() + + def test_ensure_shares_mounted_should_not_save_mounting_with_error(self): + """_ensure_shares_mounted should not save share if failed to mount.""" + mox = self._mox + drv = self._driver + + mox.StubOutWithMock(drv, '_load_shares_config') + drv._load_shares_config().AndReturn([self.TEST_EXPORT1]) + mox.StubOutWithMock(drv, '_ensure_share_mounted') + drv._ensure_share_mounted(self.TEST_EXPORT1).AndRaise(Exception()) + + mox.ReplayAll() + + drv._ensure_shares_mounted() + + self.assertEqual(0, len(drv._mounted_shares)) + + mox.VerifyAll() + + def test_setup_should_throw_error_if_shares_config_not_configured(self): + """do_setup should throw error if shares config is not configured.""" + drv = self._driver + + glusterfs.FLAGS.glusterfs_shares_config = self.TEST_SHARES_CONFIG_FILE + + self.assertRaises(exception.GlusterfsException, + drv.do_setup, IsA(context.RequestContext)) + + def test_setup_should_throw_exception_if_client_is_not_installed(self): + """do_setup should throw exception if client is not installed.""" + mox = self._mox + drv = self._driver + + glusterfs.FLAGS.glusterfs_shares_config = self.TEST_SHARES_CONFIG_FILE + + mox.StubOutWithMock(os.path, 'exists') + os.path.exists(self.TEST_SHARES_CONFIG_FILE).AndReturn(True) + mox.StubOutWithMock(drv, '_execute') + drv._execute('mount.glusterfs', check_exit_code=False).\ + AndRaise(OSError(errno.ENOENT, 'No such file or directory')) + + mox.ReplayAll() + + self.assertRaises(exception.GlusterfsException, + drv.do_setup, IsA(context.RequestContext)) + + mox.VerifyAll() + + def test_find_share_should_throw_error_if_there_is_no_mounted_shares(self): + """_find_share should throw error if there is no mounted shares.""" + 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.""" + mox = self._mox + drv = self._driver + + drv._mounted_shares = [self.TEST_EXPORT1, self.TEST_EXPORT2] + + mox.StubOutWithMock(drv, '_get_available_capacity') + drv._get_available_capacity(self.TEST_EXPORT1).\ + AndReturn(2 * self.ONE_GB_IN_BYTES) + drv._get_available_capacity(self.TEST_EXPORT2).\ + AndReturn(3 * self.ONE_GB_IN_BYTES) + + mox.ReplayAll() + + self.assertEqual(self.TEST_EXPORT2, + drv._find_share(self.TEST_SIZE_IN_GB)) + + mox.VerifyAll() + + def test_find_share_should_throw_error_if_there_is_no_enough_place(self): + """_find_share should throw error if there is no share to host vol.""" + mox = self._mox + drv = self._driver + + drv._mounted_shares = [self.TEST_EXPORT1, + self.TEST_EXPORT2] + + mox.StubOutWithMock(drv, '_get_available_capacity') + drv._get_available_capacity(self.TEST_EXPORT1).\ + AndReturn(0) + drv._get_available_capacity(self.TEST_EXPORT2).\ + AndReturn(0) + + mox.ReplayAll() + + self.assertRaises(exception.GlusterfsNoSuitableShareFound, + drv._find_share, + self.TEST_SIZE_IN_GB) + + mox.VerifyAll() + + def _simple_volume(self): + volume = DumbVolume() + volume['provider_location'] = '127.0.0.1:/mnt' + volume['name'] = 'volume_name' + volume['size'] = 10 + + return volume + + def test_create_sparsed_volume(self): + mox = self._mox + drv = self._driver + volume = self._simple_volume() + + setattr(glusterfs.FLAGS, 'glusterfs_sparsed_volumes', True) + + mox.StubOutWithMock(drv, '_create_sparsed_file') + mox.StubOutWithMock(drv, '_set_rw_permissions_for_all') + + drv._create_sparsed_file(IgnoreArg(), IgnoreArg()) + drv._set_rw_permissions_for_all(IgnoreArg()) + + mox.ReplayAll() + + drv._do_create_volume(volume) + + mox.VerifyAll() + + delattr(glusterfs.FLAGS, 'glusterfs_sparsed_volumes') + + def test_create_nonsparsed_volume(self): + mox = self._mox + drv = self._driver + volume = self._simple_volume() + + setattr(glusterfs.FLAGS, 'glusterfs_sparsed_volumes', False) + + mox.StubOutWithMock(drv, '_create_regular_file') + mox.StubOutWithMock(drv, '_set_rw_permissions_for_all') + + drv._create_regular_file(IgnoreArg(), IgnoreArg()) + drv._set_rw_permissions_for_all(IgnoreArg()) + + mox.ReplayAll() + + drv._do_create_volume(volume) + + mox.VerifyAll() + + delattr(glusterfs.FLAGS, 'glusterfs_sparsed_volumes') + + def test_create_volume_should_ensure_glusterfs_mounted(self): + """create_volume ensures shares provided in config are mounted.""" + mox = self._mox + drv = self._driver + + self.stub_out_not_replaying(glusterfs, 'LOG') + self.stub_out_not_replaying(drv, '_find_share') + self.stub_out_not_replaying(drv, '_do_create_volume') + + mox.StubOutWithMock(drv, '_ensure_shares_mounted') + drv._ensure_shares_mounted() + + mox.ReplayAll() + + volume = DumbVolume() + volume['size'] = self.TEST_SIZE_IN_GB + drv.create_volume(volume) + + mox.VerifyAll() + + def test_create_volume_should_return_provider_location(self): + """create_volume should return provider_location with found share.""" + mox = self._mox + drv = self._driver + + self.stub_out_not_replaying(glusterfs, 'LOG') + self.stub_out_not_replaying(drv, '_ensure_shares_mounted') + self.stub_out_not_replaying(drv, '_do_create_volume') + + mox.StubOutWithMock(drv, '_find_share') + drv._find_share(self.TEST_SIZE_IN_GB).AndReturn(self.TEST_EXPORT1) + + mox.ReplayAll() + + volume = DumbVolume() + volume['size'] = self.TEST_SIZE_IN_GB + result = drv.create_volume(volume) + self.assertEqual(self.TEST_EXPORT1, result['provider_location']) + + mox.VerifyAll() + + def test_delete_volume(self): + """delete_volume simple test case.""" + mox = self._mox + drv = self._driver + + self.stub_out_not_replaying(drv, '_ensure_share_mounted') + + volume = DumbVolume() + volume['name'] = 'volume-123' + volume['provider_location'] = self.TEST_EXPORT1 + + mox.StubOutWithMock(drv, 'local_path') + drv.local_path(volume).AndReturn(self.TEST_LOCAL_PATH) + + mox.StubOutWithMock(drv, '_path_exists') + drv._path_exists(self.TEST_LOCAL_PATH).AndReturn(True) + + mox.StubOutWithMock(drv, '_execute') + drv._execute('rm', '-f', self.TEST_LOCAL_PATH, run_as_root=True) + + mox.ReplayAll() + + drv.delete_volume(volume) + + mox.VerifyAll() + + def test_delete_should_ensure_share_mounted(self): + """delete_volume should ensure that corresponding share is mounted.""" + mox = self._mox + drv = self._driver + + self.stub_out_not_replaying(drv, '_execute') + + volume = DumbVolume() + volume['name'] = 'volume-123' + volume['provider_location'] = self.TEST_EXPORT1 + + mox.StubOutWithMock(drv, '_ensure_share_mounted') + drv._ensure_share_mounted(self.TEST_EXPORT1) + + mox.ReplayAll() + + drv.delete_volume(volume) + + mox.VerifyAll() + + def test_delete_should_not_delete_if_provider_location_not_provided(self): + """delete_volume shouldn't delete if provider_location missed.""" + mox = self._mox + drv = self._driver + + self.stub_out_not_replaying(drv, '_ensure_share_mounted') + + volume = DumbVolume() + volume['name'] = 'volume-123' + volume['provider_location'] = None + + mox.StubOutWithMock(drv, '_execute') + + mox.ReplayAll() + + drv.delete_volume(volume) + + mox.VerifyAll() + + def test_delete_should_not_delete_if_there_is_no_file(self): + """delete_volume should not try to delete if file missed.""" + mox = self._mox + drv = self._driver + + self.stub_out_not_replaying(drv, '_ensure_share_mounted') + + volume = DumbVolume() + volume['name'] = 'volume-123' + volume['provider_location'] = self.TEST_EXPORT1 + + mox.StubOutWithMock(drv, 'local_path') + drv.local_path(volume).AndReturn(self.TEST_LOCAL_PATH) + + mox.StubOutWithMock(drv, '_path_exists') + drv._path_exists(self.TEST_LOCAL_PATH).AndReturn(False) + + mox.StubOutWithMock(drv, '_execute') + + mox.ReplayAll() + + drv.delete_volume(volume) + + mox.VerifyAll() diff --git a/cinder/volume/drivers/glusterfs.py b/cinder/volume/drivers/glusterfs.py new file mode 100644 index 000000000..9d9991016 --- /dev/null +++ b/cinder/volume/drivers/glusterfs.py @@ -0,0 +1,250 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 cinder import exception +from cinder import flags +from cinder.openstack.common import cfg +from cinder.openstack.common import log as logging +from cinder.volume.drivers import nfs + +LOG = logging.getLogger(__name__) + +volume_opts = [ + cfg.StrOpt('glusterfs_shares_config', + default=None, + help='File with the list of available gluster shares'), + cfg.StrOpt('glusterfs_mount_point_base', + default='$state_path/mnt', + help='Base dir where gluster expected to be mounted'), + cfg.StrOpt('glusterfs_disk_util', + default='df', + help='Use du or df for free space calculation'), + cfg.BoolOpt('glusterfs_sparsed_volumes', + default=True, + help=('Create volumes as sparsed 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.'))] + +FLAGS = flags.FLAGS +FLAGS.register_opts(volume_opts) + + +class GlusterfsDriver(nfs.RemoteFsDriver): + """Gluster based cinder driver. Creates file on Gluster share for using it + as block device on hypervisor.""" + + def do_setup(self, context): + """Any initialization the volume driver does while starting.""" + super(GlusterfsDriver, self).do_setup(context) + + config = FLAGS.glusterfs_shares_config + if not config: + msg = (_("There's no Gluster config file configured (%s)") % + 'glusterfs_shares_config') + LOG.warn(msg) + raise exception.GlusterfsException(msg) + if not os.path.exists(config): + msg = (_("Gluster config file at %(config)s doesn't exist") % + locals()) + LOG.warn(msg) + raise exception.GlusterfsException(msg) + + try: + self._execute('mount.glusterfs', check_exit_code=False) + except OSError as exc: + if exc.errno == errno.ENOENT: + raise exception.GlusterfsException( + _('mount.glusterfs is not installed')) + else: + raise + + def check_for_setup_error(self): + """Just to override parent behavior.""" + pass + + def create_cloned_volume(self, volume, src_vref): + raise NotImplementedError() + + def create_volume(self, volume): + """Creates a volume.""" + + self._ensure_shares_mounted() + + volume['provider_location'] = self._find_share(volume['size']) + + LOG.info(_('casted to %s') % volume['provider_location']) + + self._do_create_volume(volume) + + return {'provider_location': volume['provider_location']} + + def delete_volume(self, volume): + """Deletes a logical volume.""" + + if not volume['provider_location']: + LOG.warn(_('Volume %s does not have provider_location specified, ' + 'skipping'), volume['name']) + return + + self._ensure_share_mounted(volume['provider_location']) + + mounted_path = self.local_path(volume) + + if not self._path_exists(mounted_path): + volume = volume['name'] + + LOG.warn(_('Trying to delete non-existing volume %(volume)s at ' + 'path %(mounted_path)s') % locals()) + return + + self._execute('rm', '-f', mounted_path, run_as_root=True) + + def ensure_export(self, ctx, volume): + """Synchronously recreates an export for a logical volume.""" + self._ensure_share_mounted(volume['provider_location']) + + def create_export(self, ctx, volume): + """Exports the volume. Can optionally return a Dictionary of changes + to the volume object to be persisted.""" + pass + + def remove_export(self, ctx, volume): + """Removes an export for a logical volume.""" + pass + + def initialize_connection(self, volume, connector): + """Allow connection to connector and return connection info.""" + data = {'export': volume['provider_location'], + 'name': volume['name']} + return { + 'driver_volume_type': 'glusterfs', + 'data': data + } + + def terminate_connection(self, volume, connector, **kwargs): + """Disallow connection from connector.""" + pass + + def _do_create_volume(self, volume): + """Create a volume on given glusterfs_share. + :param volume: volume reference + """ + volume_path = self.local_path(volume) + volume_size = volume['size'] + + if FLAGS.glusterfs_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 _ensure_shares_mounted(self): + """Look for GlusterFS shares in the flags and try to mount them + locally.""" + self._mounted_shares = [] + + for share in self._load_shares_config(): + try: + self._ensure_share_mounted(share) + self._mounted_shares.append(share) + except Exception, exc: + LOG.warning(_('Exception during mounting %s') % (exc,)) + + LOG.debug('Available shares %s' % str(self._mounted_shares)) + + def _load_shares_config(self): + return [share.strip() for share in open(FLAGS.glusterfs_shares_config) + if share and not share.startswith('#')] + + def _ensure_share_mounted(self, glusterfs_share): + """Mount GlusterFS share. + :param glusterfs_share: + """ + mount_path = self._get_mount_point_for_share(glusterfs_share) + self._mount_glusterfs(glusterfs_share, mount_path, ensure=True) + + def _find_share(self, volume_size_for): + """Choose GlusterFS share among available ones for given volume size. + Current implementation looks for greatest capacity. + :param volume_size_for: int size in GB + """ + + if not self._mounted_shares: + raise exception.GlusterfsNoSharesMounted() + + greatest_size = 0 + greatest_share = None + + for glusterfs_share in self._mounted_shares: + capacity = self._get_available_capacity(glusterfs_share) + if capacity > greatest_size: + greatest_share = glusterfs_share + greatest_size = capacity + + if volume_size_for * 1024 * 1024 * 1024 > greatest_size: + raise exception.GlusterfsNoSuitableShareFound( + volume_size=volume_size_for) + return greatest_share + + def _get_mount_point_for_share(self, glusterfs_share): + """Return mount point for share. + :param glusterfs_share: example 172.18.194.100:/var/glusterfs + """ + return os.path.join(FLAGS.glusterfs_mount_point_base, + self._get_hash_str(glusterfs_share)) + + def _get_available_capacity(self, glusterfs_share): + """Calculate available space on the GlusterFS share. + :param glusterfs_share: example 172.18.194.100:/var/glusterfs + """ + mount_point = self._get_mount_point_for_share(glusterfs_share) + + out, _ = self._execute('df', '--portability', '--block-size', '1', + mount_point, run_as_root=True) + out = out.splitlines()[1] + + available = 0 + + if FLAGS.glusterfs_disk_util == 'df': + available = int(out.split()[3]) + else: + size = int(out.split()[1]) + out, _ = self._execute('du', '-sb', '--apparent-size', + '--exclude', '*snapshot*', mount_point, + run_as_root=True) + used = int(out.split()[0]) + available = size - used + + return available + + def _mount_glusterfs(self, glusterfs_share, mount_path, ensure=False): + """Mount GlusterFS share to mount path.""" + if not self._path_exists(mount_path): + self._execute('mkdir', '-p', mount_path) + + try: + self._execute('mount', '-t', 'glusterfs', glusterfs_share, + mount_path, run_as_root=True) + except exception.ProcessExecutionError as exc: + if ensure and 'already mounted' in exc.stderr: + LOG.warn(_("%s is already mounted"), glusterfs_share) + else: + raise diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index e7b2c9463..9dfd21ca2 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -23,11 +23,11 @@ # A logging.Formatter log message format string which may use # any of the available logging.LogRecord attributes. Default: -# %default (string value) +# %(default)s (string value) #log_format=%(asctime)s %(levelname)8s [%(name)s] %(message)s -# Format string for %(asctime)s in log records. Default: -# %default (string value) +# Format string for %%(asctime)s in log records. Default: +# %(default)s (string value) #log_date_format=%Y-%m-%d %H:%M:%S # (Optional) Name of log file to output to. If not set, @@ -35,7 +35,7 @@ #log_file= # (Optional) The directory to keep log files in (will be -# prepended to --logfile) (string value) +# prepended to --log-file) (string value) #log_dir= # Use syslog for logging. (boolean value) @@ -644,9 +644,6 @@ # Options defined in cinder.scheduler.host_manager # -# num_shell_tries=3 -#### (IntOpt) number of times to attempt to run flakey shell commands - # Which filter class names to use for filtering hosts when not # specified in the request. (list value) #scheduler_default_filters=AvailabilityZoneFilter,CapacityFilter,CapabilitiesFilter @@ -731,6 +728,26 @@ #iscsi_port=3260 +# +# Options defined in cinder.volume.drivers.glusterfs +# + +# File with the list of available gluster shares (string +# value) +#glusterfs_shares_config= + +# Base dir where gluster expected to be mounted (string value) +#glusterfs_mount_point_base=$state_path/mnt + +# Use du or df for free space calculation (string value) +#glusterfs_disk_util=df + +# Create volumes as sparsed 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. (boolean value) +#glusterfs_sparsed_volumes=true + + # # Options defined in cinder.volume.drivers.lvm # @@ -747,16 +764,15 @@ # (integer value) #volume_clear_size=0 +# Size of thin provisioning pool (None uses entire cinder VG) +# (string value) +#pool_size= + # If set, create lvms with multiple mirrors. Note that this # requires lvm_mirrors + 2 pvs with available space (integer # value) #lvm_mirrors=0 -# pool_size=None -### (strOpt) Size of thin pool to create including suffix (5G), -### default is None and uses full size of VG### - - # # Options defined in cinder.volume.drivers.netapp @@ -1109,4 +1125,4 @@ #volume_driver=cinder.volume.drivers.lvm.LVMISCSIDriver -# Total option count: 244 +# Total option count: 249