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:
185
cinder/tests/test_scality.py
Normal file
185
cinder/tests/test_scality.py
Normal 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))
|
||||
259
cinder/volume/drivers/scality.py
Normal file
259
cinder/volume/drivers/scality.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user