]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add a volume driver in Cinder for Scality SOFS
authorJean-Marc Saffroy <jean.marc.saffroy@scality.com>
Mon, 14 Jan 2013 11:19:52 +0000 (12:19 +0100)
committerJean-Marc Saffroy <jean.marc.saffroy@scality.com>
Tue, 19 Feb 2013 11:21:03 +0000 (12:21 +0100)
Scality SOFS is a network filesystem mounted with FUSE, with most
options given in a configuration file. Given a mount point and a SOFS
configuration file as driver options, the Scality volume driver mounts
SOFS, and then creates, accesses and deletes volumes as regular
(sparse) files on SOFS.

Change-Id: I914714e8547a505109514e2072f9e258abca8bd4
Implements: blueprint scality-volume-driver

cinder/tests/test_scality.py [new file with mode: 0644]
cinder/volume/drivers/scality.py [new file with mode: 0644]
etc/cinder/cinder.conf.sample
etc/cinder/rootwrap.d/volume.filters

diff --git a/cinder/tests/test_scality.py b/cinder/tests/test_scality.py
new file mode 100644 (file)
index 0000000..f32261c
--- /dev/null
@@ -0,0 +1,185 @@
+# Copyright (c) 2013 Scality
+#
+#    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 Scality SOFS Volume Driver.
+"""
+
+import errno
+import os
+
+from cinder import exception
+from cinder import test
+from cinder import utils
+from cinder.volume.drivers import scality
+
+
+class ScalityDriverTestCase(test.TestCase):
+    """Test case for the Scality driver."""
+
+    TEST_MOUNT = '/tmp/fake_mount'
+    TEST_CONFIG = '/tmp/fake_config'
+    TEST_VOLDIR = 'volumes'
+
+    TEST_VOLNAME = 'volume_name'
+    TEST_VOLSIZE = '0'
+    TEST_VOLUME = {
+        'name': TEST_VOLNAME,
+        'size': TEST_VOLSIZE
+    }
+    TEST_VOLPATH = os.path.join(TEST_MOUNT,
+                                TEST_VOLDIR,
+                                TEST_VOLNAME)
+
+    TEST_SNAPNAME = 'snapshot_name'
+    TEST_SNAPSHOT = {
+        'name': TEST_SNAPNAME,
+        'volume_name': TEST_VOLNAME,
+        'volume_size': TEST_VOLSIZE
+    }
+    TEST_SNAPPATH = os.path.join(TEST_MOUNT,
+                                 TEST_VOLDIR,
+                                 TEST_SNAPNAME)
+
+    def _makedirs(self, path):
+        try:
+            os.makedirs(path)
+        except OSError as e:
+            if e.errno != errno.EEXIST:
+                raise e
+
+    def _create_fake_config(self):
+        open(self.TEST_CONFIG, "w+").close()
+
+    def _create_fake_mount(self):
+        self._makedirs(os.path.join(self.TEST_MOUNT, 'sys'))
+        self._makedirs(os.path.join(self.TEST_MOUNT, self.TEST_VOLDIR))
+
+    def _remove_fake_mount(self):
+        utils.execute('rm', '-rf', self.TEST_MOUNT)
+
+    def _remove_fake_config(self):
+        try:
+            os.unlink(self.TEST_CONFIG)
+        except OSError as e:
+            if e.errno != errno.ENOENT:
+                raise e
+
+    def _configure_driver(self):
+        scality.FLAGS.scality_sofs_config = self.TEST_CONFIG
+        scality.FLAGS.scality_sofs_mount_point = self.TEST_MOUNT
+        scality.FLAGS.scality_sofs_volume_dir = self.TEST_VOLDIR
+
+    def _execute_wrapper(self, cmd, *args, **kwargs):
+        try:
+            kwargs.pop('run_as_root')
+        except KeyError:
+            pass
+        utils.execute(cmd, *args, **kwargs)
+
+    def _set_access_wrapper(self, is_visible):
+
+        def _access_wrapper(path, flags):
+            if path == '/sbin/mount.sofs':
+                return is_visible
+            else:
+                return os.access(path, flags)
+
+        self.stubs.Set(os, 'access', _access_wrapper)
+
+    def setUp(self):
+        super(ScalityDriverTestCase, self).setUp()
+
+        self._remove_fake_mount()
+        self._driver = scality.ScalityDriver()
+        self._driver.set_execute(self._execute_wrapper)
+
+        self._create_fake_mount()
+        self._create_fake_config()
+        self._configure_driver()
+
+    def tearDown(self):
+        self._remove_fake_mount()
+        self._remove_fake_config()
+        super(ScalityDriverTestCase, self).tearDown()
+
+    def test_setup_no_config(self):
+        """Missing SOFS configuration shall raise an error."""
+        scality.FLAGS.scality_sofs_config = None
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self._driver.do_setup, None)
+
+    def test_setup_missing_config(self):
+        """Non-existent SOFS configuration file shall raise an error."""
+        scality.FLAGS.scality_sofs_config = 'nonexistent.conf'
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self._driver.do_setup, None)
+
+    def test_setup_no_mount_helper(self):
+        """SOFS must be installed to use the driver."""
+        self._set_access_wrapper(False)
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self._driver.do_setup, None)
+
+    def test_setup_make_voldir(self):
+        """The directory for volumes shall be created automatically."""
+        self._set_access_wrapper(True)
+        voldir_path = os.path.join(self.TEST_MOUNT, self.TEST_VOLDIR)
+        os.rmdir(voldir_path)
+        self._driver.do_setup(None)
+        self.assertTrue(os.path.isdir(voldir_path))
+
+    def test_local_path(self):
+        """Expected behaviour for local_path."""
+        self.assertEqual(self._driver.local_path(self.TEST_VOLUME),
+                         self.TEST_VOLPATH)
+
+    def test_create_volume(self):
+        """Expected behaviour for create_volume."""
+        ret = self._driver.create_volume(self.TEST_VOLUME)
+        self.assertEqual(ret['provider_location'],
+                         os.path.join(self.TEST_VOLDIR,
+                                      self.TEST_VOLNAME))
+        self.assertTrue(os.path.isfile(self.TEST_VOLPATH))
+        self.assertEqual(os.stat(self.TEST_VOLPATH).st_size,
+                         100 * 1024 * 1024)
+
+    def test_delete_volume(self):
+        """Expected behaviour for delete_volume."""
+        self._driver.create_volume(self.TEST_VOLUME)
+        self._driver.delete_volume(self.TEST_VOLUME)
+        self.assertFalse(os.path.isfile(self.TEST_VOLPATH))
+
+    def test_create_snapshot(self):
+        """Expected behaviour for create_snapshot."""
+        self._driver.create_volume(self.TEST_VOLUME)
+        self._driver.create_snapshot(self.TEST_SNAPSHOT)
+        self.assertTrue(os.path.isfile(self.TEST_SNAPPATH))
+        self.assertEqual(os.stat(self.TEST_SNAPPATH).st_size,
+                         100 * 1024 * 1024)
+
+    def test_delete_snapshot(self):
+        """Expected behaviour for delete_snapshot."""
+        self._driver.create_volume(self.TEST_VOLUME)
+        self._driver.create_snapshot(self.TEST_SNAPSHOT)
+        self._driver.delete_snapshot(self.TEST_SNAPSHOT)
+        self.assertFalse(os.path.isfile(self.TEST_SNAPPATH))
+
+    def test_initialize_connection(self):
+        """Expected behaviour for initialize_connection."""
+        ret = self._driver.initialize_connection(self.TEST_VOLUME, None)
+        self.assertEqual(ret['driver_volume_type'], 'scality')
+        self.assertEqual(ret['data']['sofs_path'],
+                         os.path.join(self.TEST_VOLDIR,
+                                      self.TEST_VOLNAME))
diff --git a/cinder/volume/drivers/scality.py b/cinder/volume/drivers/scality.py
new file mode 100644 (file)
index 0000000..67574f8
--- /dev/null
@@ -0,0 +1,259 @@
+# Copyright (c) 2013 Scality
+#
+#    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.
+
+"""
+Scality SOFS Volume Driver.
+"""
+
+import errno
+import os
+import urllib2
+import urlparse
+
+from cinder import exception
+from cinder import flags
+from cinder.image import image_utils
+from cinder.openstack.common import cfg
+from cinder.openstack.common import log as logging
+from cinder.volume import driver
+
+LOG = logging.getLogger(__name__)
+
+volume_opts = [
+    cfg.StrOpt('scality_sofs_config',
+               default=None,
+               help='Path or URL to Scality SOFS configuration file'),
+    cfg.StrOpt('scality_sofs_mount_point',
+               default='$state_path/scality',
+               help='Base dir where Scality SOFS shall be mounted'),
+    cfg.StrOpt('scality_sofs_volume_dir',
+               default='cinder/volumes',
+               help='Path from Scality SOFS root to volume dir'),
+]
+
+FLAGS = flags.FLAGS
+FLAGS.register_opts(volume_opts)
+
+
+class ScalityDriver(driver.VolumeDriver):
+    """Scality SOFS cinder driver.
+
+    Creates sparse files on SOFS for hypervisors to use as block
+    devices.
+    """
+
+    def _check_prerequisites(self):
+        """Sanity checks before attempting to mount SOFS."""
+
+        # config is mandatory
+        config = FLAGS.scality_sofs_config
+        if not config:
+            msg = _("Value required for 'scality_sofs_config'")
+            LOG.warn(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+        # config can be a file path or a URL, check it
+        if urlparse.urlparse(config).scheme == '':
+            # turn local path into URL
+            config = 'file://%s' % config
+        try:
+            urllib2.urlopen(config, timeout=5).close()
+        except urllib2.URLError as e:
+            msg = _("Cannot access 'scality_sofs_config': %s") % e
+            LOG.warn(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+        # mount.sofs must be installed
+        if not os.access('/sbin/mount.sofs', os.X_OK):
+            msg = _("Cannot execute /sbin/mount.sofs")
+            LOG.warn(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+    def _makedirs(self, path):
+        try:
+            os.makedirs(path)
+        except OSError as e:
+            if e.errno != errno.EEXIST:
+                raise e
+
+    def _mount_sofs(self):
+        config = FLAGS.scality_sofs_config
+        mount_path = FLAGS.scality_sofs_mount_point
+        sysdir = os.path.join(mount_path, 'sys')
+
+        self._makedirs(mount_path)
+        if not os.path.isdir(sysdir):
+            self._execute('mount', '-t', 'sofs', config, mount_path,
+                          run_as_root=True)
+        if not os.path.isdir(sysdir):
+            msg = _("Cannot mount Scality SOFS, check syslog for errors")
+            LOG.warn(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+    def _size_bytes(self, size_in_g):
+        if int(size_in_g) == 0:
+            return 100 * 1024 * 1024
+        return int(size_in_g) * 1024 * 1024 * 1024
+
+    def _create_file(self, path, size):
+        with open(path, "ab") as f:
+            f.truncate(size)
+        os.chmod(path, 0666)
+
+    def _copy_file(self, src_path, dest_path):
+        self._execute('dd', 'if=%s' % src_path, 'of=%s' % dest_path,
+                      'bs=1M', 'conv=fsync,nocreat,notrunc',
+                      run_as_root=True)
+
+    def do_setup(self, context):
+        """Any initialization the volume driver does while starting."""
+        self._check_prerequisites()
+        self._mount_sofs()
+        voldir = os.path.join(FLAGS.scality_sofs_mount_point,
+                              FLAGS.scality_sofs_volume_dir)
+        if not os.path.isdir(voldir):
+            self._makedirs(voldir)
+
+    def check_for_setup_error(self):
+        """Returns an error if prerequisites aren't met."""
+        self._check_prerequisites()
+        voldir = os.path.join(FLAGS.scality_sofs_mount_point,
+                              FLAGS.scality_sofs_volume_dir)
+        if not os.path.isdir(voldir):
+            msg = _("Cannot find volume dir for Scality SOFS at '%s'") % voldir
+            LOG.warn(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+    def create_volume(self, volume):
+        """Creates a logical volume.
+
+        Can optionally return a Dictionary of changes to the volume
+        object to be persisted.
+        """
+        self._create_file(self.local_path(volume),
+                          self._size_bytes(volume['size']))
+        volume['provider_location'] = self._sofs_path(volume)
+        return {'provider_location': volume['provider_location']}
+
+    def create_volume_from_snapshot(self, volume, snapshot):
+        """Creates a volume from a snapshot."""
+        changes = self.create_volume(volume)
+        self._copy_file(self.local_path(snapshot),
+                        self.local_path(volume))
+        return changes
+
+    def delete_volume(self, volume):
+        """Deletes a logical volume."""
+        os.remove(self.local_path(volume))
+
+    def create_snapshot(self, snapshot):
+        """Creates a snapshot."""
+        volume_path = os.path.join(FLAGS.scality_sofs_mount_point,
+                                   FLAGS.scality_sofs_volume_dir,
+                                   snapshot['volume_name'])
+        snapshot_path = self.local_path(snapshot)
+        self._create_file(snapshot_path,
+                          self._size_bytes(snapshot['volume_size']))
+        self._copy_file(volume_path, snapshot_path)
+
+    def delete_snapshot(self, snapshot):
+        """Deletes a snapshot."""
+        os.remove(self.local_path(snapshot))
+
+    def _sofs_path(self, volume):
+        return os.path.join(FLAGS.scality_sofs_volume_dir,
+                            volume['name'])
+
+    def local_path(self, volume):
+        return os.path.join(FLAGS.scality_sofs_mount_point,
+                            self._sofs_path(volume))
+
+    def ensure_export(self, context, volume):
+        """Synchronously recreates an export for a logical volume."""
+        pass
+
+    def create_export(self, context, volume):
+        """Exports the volume.
+
+        Can optionally return a Dictionary of changes to the volume
+        object to be persisted.
+        """
+        pass
+
+    def remove_export(self, context, volume):
+        """Removes an export for a logical volume."""
+        pass
+
+    def initialize_connection(self, volume, connector):
+        """Allow connection to connector and return connection info."""
+        return {
+            'driver_volume_type': 'scality',
+            'data': {
+                'sofs_path': self._sofs_path(volume),
+            }
+        }
+
+    def terminate_connection(self, volume, connector, force=False, **kwargs):
+        """Disallow connection from connector."""
+        pass
+
+    def attach_volume(self, context, volume_id, instance_uuid, mountpoint):
+        """ Callback for volume attached to instance."""
+        pass
+
+    def detach_volume(self, context, volume_id):
+        """ Callback for volume detached."""
+        pass
+
+    def get_volume_stats(self, refresh=False):
+        """Return the current state of the volume service.
+
+        If 'refresh' is True, run the update first.
+        """
+        stats = {
+            'volume_backend_name': 'Scality_SOFS',
+            'vendor_name': 'Scality',
+            'driver_version': '1.0',
+            'storage_protocol': 'scality',
+            'total_capacity_gb': 'infinite',
+            'free_capacity_gb': 'infinite',
+            'reserved_percentage': 0,
+        }
+        return stats
+
+    def copy_image_to_volume(self, context, volume, image_service, image_id):
+        """Fetch the image from image_service and write it to the volume."""
+        image_utils.fetch_to_raw(context,
+                                 image_service,
+                                 image_id,
+                                 self.local_path(volume))
+        self.create_volume(volume)
+
+    def copy_volume_to_image(self, context, volume, image_service, image_meta):
+        """Copy the volume to the specified image."""
+        image_utils.upload_volume(context,
+                                  image_service,
+                                  image_meta,
+                                  self.local_path(volume))
+
+    def clone_image(self, volume, image_location):
+        """Create a volume efficiently from an existing image.
+
+        image_location is a string whose format depends on the
+        image service backend in use. The driver should use it
+        to determine whether cloning is possible.
+
+        Returns a boolean indicating whether cloning occurred
+        """
+        return False
index f5b0d0bb209e75aaea8bd611834252fd89c6c676..2156560efa0c7a39e16697696fc14b6a2965d9f2 100644 (file)
 #san_zfs_volume_base=rpool/
 
 
+#
+# Options defined in cinder.volume.drivers.scality
+#
+
+# Path or URL to Scality SOFS configuration file (string
+# value)
+#scality_sofs_config=<None>
+
+# Base dir where Scality SOFS shall be mounted (string value)
+#scality_sofs_mount_point=$state_path/scality
+
+# Path from Scality SOFS root to volume dir (string value)
+#scality_sofs_volume_dir=cinder/volumes
+
+
 #
 # Options defined in cinder.volume.drivers.solidfire
 #
 #volume_driver=cinder.volume.driver.FakeISCSIDriver
 
 
-# Total option count: 251
+# Total option count: 254
index 3a22fa9cbfe3d66eb5abd8939037dc5e97bbee69..d5c954b8d5ce655f78ca0f330f5df9e4b4f9a043 100644 (file)
@@ -54,3 +54,7 @@ truncate: CommandFilter, /usr/bin/truncate, root
 chmod: CommandFilter, /bin/chmod, root
 rm: CommandFilter, /bin/rm, root
 lvs: CommandFilter, /sbin/lvs, root
+
+# cinder/volume/scality.py
+mount: CommandFilter, /bin/mount, root
+dd: CommandFilter, /bin/dd, root