Add a volume driver in Cinder for Scality SOFS

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
This commit is contained in:
Jean-Marc Saffroy
2013-01-14 12:19:52 +01:00
parent e1815e8673
commit e5b1d51117
4 changed files with 464 additions and 1 deletions

View File

@@ -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))

View File

@@ -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

View File

@@ -977,6 +977,21 @@
#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
#
@@ -1144,4 +1159,4 @@
#volume_driver=cinder.volume.driver.FakeISCSIDriver
# Total option count: 251
# Total option count: 254

View 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