cinder/cinder/volume/drivers/scality.py

322 lines
12 KiB
Python

# 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 os
from oslo_concurrency import lockutils
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import units
from six.moves import urllib
from cinder import exception
from cinder.i18n import _, _LI
from cinder.image import image_utils
from cinder.openstack.common import fileutils
from cinder import utils
from cinder.volume import driver
from cinder.volume import utils as volume_utils
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'),
]
CONF = cfg.CONF
CONF.register_opts(volume_opts)
class ScalityDriver(driver.VolumeDriver):
"""Scality SOFS cinder driver.
Creates sparse files on SOFS for hypervisors to use as block
devices.
"""
VERSION = '1.0.0'
def __init__(self, *args, **kwargs):
super(ScalityDriver, self).__init__(*args, **kwargs)
self.configuration.append_config_values(volume_opts)
def _check_prerequisites(self):
"""Sanity checks before attempting to mount SOFS."""
# config is mandatory
config = self.configuration.scality_sofs_config
if not config:
msg = _("Value required for 'scality_sofs_config'")
LOG.warning(msg)
raise exception.VolumeBackendAPIException(data=msg)
# config can be a file path or a URL, check it
if urllib.parse.urlparse(config).scheme == '':
# turn local path into URL
config = 'file://%s' % config
try:
urllib.request.urlopen(config, timeout=5).close()
except urllib.error.URLError as e:
msg = _("Cannot access 'scality_sofs_config': %s") % e
LOG.warning(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.warning(msg)
raise exception.VolumeBackendAPIException(data=msg)
@lockutils.synchronized('mount-sofs', 'cinder-sofs', external=True)
def _mount_sofs(self):
config = self.configuration.scality_sofs_config
mount_path = self.configuration.scality_sofs_mount_point
fileutils.ensure_tree(mount_path)
if not self._sofs_is_mounted():
self._execute('mount', '-t', 'sofs', config, mount_path,
run_as_root=True)
if not self._sofs_is_mounted():
msg = _("Cannot mount Scality SOFS, check syslog for errors")
LOG.warning(msg)
raise exception.VolumeBackendAPIException(data=msg)
def _sofs_is_mounted(self):
mount_path = self.configuration.scality_sofs_mount_point.rstrip('/')
for mount in volume_utils.read_proc_mounts():
parts = mount.split()
if (parts[0].endswith('fuse') and
parts[1].rstrip('/') == mount_path):
return True
return False
def _size_bytes(self, size_in_g):
return int(size_in_g) * units.Gi
def _create_file(self, path, size):
with open(path, "ab") as f:
f.truncate(size)
os.chmod(path, 0o666)
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(self.configuration.scality_sofs_mount_point,
self.configuration.scality_sofs_volume_dir)
fileutils.ensure_tree(voldir)
def check_for_setup_error(self):
"""Returns an error if prerequisites aren't met."""
self._check_prerequisites()
voldir = os.path.join(self.configuration.scality_sofs_mount_point,
self.configuration.scality_sofs_volume_dir)
if not os.path.isdir(voldir):
msg = _("Cannot find volume dir for Scality SOFS at '%s'") % voldir
LOG.warning(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(self.configuration.scality_sofs_mount_point,
self.configuration.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(self.configuration.scality_sofs_volume_dir,
volume['name'])
def local_path(self, volume):
return os.path.join(self.configuration.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),
'export': self.configuration.scality_sofs_volume_dir,
'name': volume['name'],
}
}
def terminate_connection(self, volume, connector, **kwargs):
"""Disallow connection from connector."""
pass
def detach_volume(self, context, volume, attachment=None):
"""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 = {
'vendor_name': 'Scality',
'driver_version': self.VERSION,
'storage_protocol': 'scality',
'total_capacity_gb': 'infinite',
'free_capacity_gb': 'infinite',
'reserved_percentage': 0,
}
backend_name = self.configuration.safe_get('volume_backend_name')
stats['volume_backend_name'] = backend_name or 'Scality_SOFS'
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.configuration.volume_dd_blocksize,
size=volume['size'])
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, context, volume,
image_location, image_meta,
image_service):
"""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.
image_meta is the metadata associated with the image and
includes properties like the image id, size, virtual-size
etc.
image_service is the reference of the image_service to use.
Note that this is needed to be passed here for drivers that
will want to fetch images from the image service directly.
Returns a dict of volume properties eg. provider_location,
boolean indicating whether cloning occurred
"""
return None, False
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
self.create_volume_from_snapshot(volume, src_vref)
def extend_volume(self, volume, new_size):
"""Extend an existing volume."""
self._create_file(self.local_path(volume),
self._size_bytes(new_size))
def backup_volume(self, context, backup, backup_service):
"""Create a new backup from an existing volume."""
volume = self.db.volume_get(context, backup['volume_id'])
volume_local_path = self.local_path(volume)
LOG.info(_LI('Begin backup of volume %s.'), volume['name'])
qemu_img_info = image_utils.qemu_img_info(volume_local_path)
if qemu_img_info.file_format != 'raw':
msg = _('Backup is only supported for raw-formatted '
'SOFS volumes.')
raise exception.InvalidVolume(msg)
if qemu_img_info.backing_file is not None:
msg = _('Backup is only supported for SOFS volumes '
'without backing file.')
raise exception.InvalidVolume(msg)
with utils.temporary_chown(volume_local_path):
with fileutils.file_open(volume_local_path) as volume_file:
backup_service.backup(backup, volume_file)
def restore_backup(self, context, backup, volume, backup_service):
"""Restore an existing backup to a new or existing volume."""
LOG.info(_LI('Restoring backup %(backup)s to volume %(volume)s.'),
{'backup': backup['id'], 'volume': volume['name']})
volume_local_path = self.local_path(volume)
with utils.temporary_chown(volume_local_path):
with fileutils.file_open(volume_local_path, 'wb') as volume_file:
backup_service.restore(backup, volume['id'], volume_file)