1a5de5d4bd
This patch adds a CI_WIKI_NAME to each driver object. The value is the exact name of the ThirdPartySystems wiki page. This allows us to create an automated tool to associated jobs to drivers and track their CI reporting status correctly. This patch also updates the generate_driver_list.py script to output the driver list as a python list of dicts that can be directly consumed. Change-Id: I0ec5f705e91f680a731648cf50738ea219565f70
303 lines
11 KiB
Python
303 lines
11 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
|
|
|
|
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)
|