]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add GlusterFS volume driver
authorEric Harney <eharney@redhat.com>
Wed, 6 Feb 2013 15:26:45 +0000 (10:26 -0500)
committerEric Harney <eharney@redhat.com>
Fri, 15 Feb 2013 16:09:55 +0000 (11:09 -0500)
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

cinder/exception.py
cinder/tests/test_glusterfs.py [new file with mode: 0644]
cinder/volume/drivers/glusterfs.py [new file with mode: 0644]
etc/cinder/cinder.conf.sample

index 39ac7098e15bc5bee24598948be261fdaa125fc4..14e1ba4b7602d6d671b4b66cb7bc08e3e23c4852 100644 (file)
@@ -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 (file)
index 0000000..191b4eb
--- /dev/null
@@ -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 (file)
index 0000000..9d99910
--- /dev/null
@@ -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
index e7b2c946380e9af62a9cbc41fbdb649033dae527..9dfd21ca2dc0a400a2b094d69a4ac6922cec2c6a 100644 (file)
 
 # 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=<None>
 
 # (Optional) The directory to keep log files in (will be
-# prepended to --logfile) (string value)
+# prepended to --log-file) (string value)
 #log_dir=<None>
 
 # Use syslog for logging. (boolean value)
 # 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
 #iscsi_port=3260
 
 
+#
+# Options defined in cinder.volume.drivers.glusterfs
+#
+
+# File with the list of available gluster shares (string
+# value)
+#glusterfs_shares_config=<None>
+
+# 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
 #
 # (integer value)
 #volume_clear_size=0
 
+# Size of thin provisioning pool (None uses entire cinder VG)
+# (string value)
+#pool_size=<None>
+
 # 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
 #volume_driver=cinder.volume.drivers.lvm.LVMISCSIDriver
 
 
-# Total option count: 244
+# Total option count: 249