aded066c99
The Scality CI has not reported on a patch since 2016-04-12 09:44:38. This is well past our policy for CI reporting. Per Cinder's non-compliance policy [1] this patch marks the driver as unsupported and deprecated and it will be approved if the issue is not corrected by the next cycle. [1] https://wiki.openstack.org/wiki/Cinder/tested-3rdParty-drivers#Non-Compliance_Policy Change-Id: Ic749db69d793359c9328c8c62b919197c1cec859
307 lines
12 KiB
Python
307 lines
12 KiB
Python
# Copyright (c) 2015 Scality
|
|
# 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.
|
|
|
|
"""
|
|
Scality SOFS Volume Driver.
|
|
"""
|
|
|
|
|
|
import errno
|
|
import os
|
|
|
|
from oslo_concurrency import lockutils
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_utils import fileutils
|
|
import six
|
|
from six.moves import urllib
|
|
|
|
from cinder import exception
|
|
from cinder.i18n import _, _LI
|
|
from cinder.image import image_utils
|
|
from cinder import interface
|
|
from cinder import utils
|
|
from cinder.volume.drivers import remotefs as remotefs_drv
|
|
from cinder.volume import utils as volume_utils
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
volume_opts = [
|
|
cfg.StrOpt('scality_sofs_config',
|
|
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)
|
|
|
|
|
|
@interface.volumedriver
|
|
class ScalityDriver(remotefs_drv.RemoteFSSnapDriver):
|
|
"""Scality SOFS cinder driver.
|
|
|
|
Creates sparse files on SOFS for hypervisors to use as block
|
|
devices.
|
|
"""
|
|
|
|
driver_volume_type = 'scality'
|
|
driver_prefix = 'scality_sofs'
|
|
volume_backend_name = 'Scality_SOFS'
|
|
VERSION = '2.0.0'
|
|
|
|
# ThirdPartySystems wiki page
|
|
CI_WIKI_NAME = "Scality_CI"
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(ScalityDriver, self).__init__(*args, **kwargs)
|
|
self.configuration.append_config_values(volume_opts)
|
|
|
|
self.sofs_mount_point = self.configuration.scality_sofs_mount_point
|
|
self.sofs_config = self.configuration.scality_sofs_config
|
|
self.sofs_rel_volume_dir = self.configuration.scality_sofs_volume_dir
|
|
self.sofs_abs_volume_dir = os.path.join(self.sofs_mount_point,
|
|
self.sofs_rel_volume_dir)
|
|
|
|
# The following config flag is used by RemoteFSDriver._do_create_volume
|
|
# We want to use sparse file (ftruncated) without exposing this
|
|
# as a config switch to customers.
|
|
self.configuration.scality_sofs_sparsed_volumes = True
|
|
|
|
# TODO(smcginnis) Either remove this if CI requirements are met, or
|
|
# remove this driver in the Ocata release per normal deprecation
|
|
self._supported = False
|
|
|
|
def check_for_setup_error(self):
|
|
"""Sanity checks before attempting to mount SOFS."""
|
|
|
|
# config is mandatory
|
|
if not self.sofs_config:
|
|
msg = _("Value required for 'scality_sofs_config'")
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
# config can be a file path or a URL, check it
|
|
config = self.sofs_config
|
|
if urllib.parse.urlparse(self.sofs_config).scheme == '':
|
|
# turn local path into URL
|
|
config = 'file://%s' % self.sofs_config
|
|
try:
|
|
urllib.request.urlopen(config, timeout=5).close()
|
|
except (urllib.error.URLError, urllib.error.HTTPError) as e:
|
|
msg = _("Can't access 'scality_sofs_config'"
|
|
": %s") % six.text_type(e)
|
|
LOG.error(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.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
def _load_shares_config(self, share_file=None):
|
|
self.shares[self.sofs_rel_volume_dir] = None
|
|
|
|
def _get_mount_point_for_share(self, share=None):
|
|
# The _qemu_img_info_base() method from the RemoteFSSnapDriver class
|
|
# expects files (volume) to be inside a subdir of the mount point.
|
|
# So we have to append a dummy subdir.
|
|
return self.sofs_abs_volume_dir + "/00"
|
|
|
|
def _sofs_is_mounted(self):
|
|
"""Check if SOFS is already mounted at the expected location."""
|
|
mount_path = self.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
|
|
|
|
@lockutils.synchronized('mount-sofs', 'cinder-sofs', external=True)
|
|
def _ensure_share_mounted(self, share=None):
|
|
"""Mount SOFS if need be."""
|
|
fileutils.ensure_tree(self.sofs_mount_point)
|
|
|
|
if not self._sofs_is_mounted():
|
|
self._execute('mount', '-t', 'sofs', self.sofs_config,
|
|
self.sofs_mount_point, run_as_root=True)
|
|
# Check whether the mount command succeeded
|
|
if not self._sofs_is_mounted():
|
|
msg = _("Cannot mount Scality SOFS, check syslog for errors")
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
fileutils.ensure_tree(self.sofs_abs_volume_dir)
|
|
|
|
# We symlink the '00' subdir to its parent dir to maintain
|
|
# compatibility with previous version of this driver.
|
|
try:
|
|
os.symlink(".", self._get_mount_point_for_share())
|
|
except OSError as exc:
|
|
if exc.errno == errno.EEXIST:
|
|
if not os.path.islink(self._get_mount_point_for_share()):
|
|
raise
|
|
else:
|
|
raise
|
|
|
|
def _ensure_shares_mounted(self):
|
|
self._ensure_share_mounted()
|
|
self._mounted_shares = [self.sofs_rel_volume_dir]
|
|
|
|
def _find_share(self, volume_size_for):
|
|
try:
|
|
return self._mounted_shares[0]
|
|
except IndexError:
|
|
raise exception.RemoteFSNoSharesMounted()
|
|
|
|
def get_volume_stats(self, refresh=False):
|
|
"""Return the current state of the volume service."""
|
|
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 self.volume_backend_name
|
|
return stats
|
|
|
|
@remotefs_drv.locked_volume_id_operation
|
|
def initialize_connection(self, volume, connector):
|
|
"""Allow connection to connector and return connection info."""
|
|
|
|
# Find active qcow2 file
|
|
active_file = self.get_active_image_from_info(volume)
|
|
path = '%s/%s' % (self._get_mount_point_for_share(), active_file)
|
|
sofs_rel_path = os.path.join(self.sofs_rel_volume_dir, "00",
|
|
volume.name)
|
|
|
|
data = {'export': volume.provider_location,
|
|
'name': active_file,
|
|
'sofs_path': sofs_rel_path}
|
|
|
|
# Test file for raw vs. qcow2 format
|
|
info = self._qemu_img_info(path, volume.name)
|
|
data['format'] = info.file_format
|
|
if data['format'] not in ['raw', 'qcow2']:
|
|
msg = _('%s must be a valid raw or qcow2 image.') % path
|
|
raise exception.InvalidVolume(msg)
|
|
|
|
return {
|
|
'driver_volume_type': self.driver_volume_type,
|
|
'data': data,
|
|
'mount_point_base': self.sofs_mount_point
|
|
}
|
|
|
|
def _qemu_img_info(self, path, volume_name):
|
|
return super(ScalityDriver, self)._qemu_img_info_base(
|
|
path, volume_name, self.sofs_abs_volume_dir)
|
|
|
|
@remotefs_drv.locked_volume_id_operation
|
|
def extend_volume(self, volume, size_gb):
|
|
volume_path = self.local_path(volume)
|
|
|
|
info = self._qemu_img_info(volume_path, volume.name)
|
|
backing_fmt = info.file_format
|
|
|
|
if backing_fmt not in ['raw', 'qcow2']:
|
|
msg = _('Unrecognized backing format: %s')
|
|
raise exception.InvalidVolume(msg % backing_fmt)
|
|
|
|
# qemu-img can resize both raw and qcow2 files
|
|
image_utils.resize_image(volume_path, size_gb)
|
|
|
|
def _copy_volume_from_snapshot(self, snapshot, volume, volume_size):
|
|
"""Copy data from snapshot to destination volume.
|
|
|
|
This is done with a qemu-img convert to raw/qcow2 from the snapshot
|
|
qcow2.
|
|
"""
|
|
|
|
info_path = self._local_path_volume_info(snapshot.volume)
|
|
|
|
# For BC compat' with version < 2 of this driver
|
|
try:
|
|
snap_info = self._read_info_file(info_path)
|
|
except IOError as exc:
|
|
if exc.errno != errno.ENOENT:
|
|
raise
|
|
else:
|
|
path_to_snap_img = self.local_path(snapshot)
|
|
else:
|
|
vol_path = self._local_volume_dir(snapshot.volume)
|
|
|
|
forward_file = snap_info[snapshot.id]
|
|
forward_path = os.path.join(vol_path, forward_file)
|
|
|
|
# Find the file which backs this file, which represents the point
|
|
# when this snapshot was created.
|
|
img_info = self._qemu_img_info(forward_path,
|
|
snapshot.volume.name)
|
|
|
|
path_to_snap_img = os.path.join(vol_path, img_info.backing_file)
|
|
|
|
LOG.debug("will copy from snapshot at %s", path_to_snap_img)
|
|
|
|
path_to_new_vol = self.local_path(volume)
|
|
out_format = 'raw'
|
|
image_utils.convert_image(path_to_snap_img,
|
|
path_to_new_vol,
|
|
out_format,
|
|
run_as_root=self._execute_as_root)
|
|
|
|
self._set_rw_permissions_for_all(path_to_new_vol)
|
|
|
|
image_utils.resize_image(path_to_new_vol, volume_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 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 open(volume_local_path, 'wb') as volume_file:
|
|
backup_service.restore(backup, volume.id, volume_file)
|