4769acf2e8
CI is now reporting and it trending towards reliable results.
This reverts commit 76ef111e4c
.
Change-Id: I2b4750604e085c10ce96778c7be032358287011f
481 lines
19 KiB
Python
481 lines
19 KiB
Python
# Copyright (c) 2014 Quobyte Inc.
|
|
# Copyright (c) 2013 Red Hat, Inc.
|
|
# 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.
|
|
|
|
import errno
|
|
import os
|
|
|
|
from oslo_concurrency import processutils
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_utils import fileutils
|
|
|
|
from cinder import compute
|
|
from cinder import exception
|
|
from cinder.i18n import _, _LI, _LW
|
|
from cinder.image import image_utils
|
|
from cinder import interface
|
|
from cinder import utils
|
|
from cinder.volume.drivers import remotefs as remotefs_drv
|
|
|
|
VERSION = '1.1'
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
volume_opts = [
|
|
cfg.URIOpt('quobyte_volume_url',
|
|
help=('URL to the Quobyte volume e.g.,'
|
|
' quobyte://<DIR host>/<volume name>')),
|
|
cfg.StrOpt('quobyte_client_cfg',
|
|
help=('Path to a Quobyte Client configuration file.')),
|
|
cfg.BoolOpt('quobyte_sparsed_volumes',
|
|
default=True,
|
|
help=('Create volumes as sparse files which take no space.'
|
|
' If set to False, volume is created as regular file.'
|
|
'In such case volume creation takes a lot of time.')),
|
|
cfg.BoolOpt('quobyte_qcow2_volumes',
|
|
default=True,
|
|
help=('Create volumes as QCOW2 files rather than raw files.')),
|
|
cfg.StrOpt('quobyte_mount_point_base',
|
|
default='$state_path/mnt',
|
|
help=('Base dir containing the mount point'
|
|
' for the Quobyte volume.')),
|
|
]
|
|
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(volume_opts)
|
|
|
|
|
|
@interface.volumedriver
|
|
class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed):
|
|
"""Cinder driver for Quobyte USP.
|
|
|
|
Volumes are stored as files on the mounted Quobyte volume. The hypervisor
|
|
will expose them as block devices.
|
|
|
|
Unlike other similar drivers, this driver uses exactly one Quobyte volume
|
|
because Quobyte USP is a distributed storage system. To add or remove
|
|
capacity, administrators can add or remove storage servers to/from the
|
|
volume.
|
|
|
|
For different types of volumes e.g., SSD vs. rotating disks,
|
|
use multiple backends in Cinder.
|
|
|
|
Note: To be compliant with the inherited RemoteFSSnapDriver, Quobyte
|
|
volumes are also referred to as shares.
|
|
|
|
Version history:
|
|
1.0 - Initial driver.
|
|
1.1 - Adds optional insecure NAS settings
|
|
"""
|
|
|
|
driver_volume_type = 'quobyte'
|
|
driver_prefix = 'quobyte'
|
|
volume_backend_name = 'Quobyte'
|
|
VERSION = VERSION
|
|
|
|
# ThirdPartySystems wiki page
|
|
CI_WIKI_NAME = "Quobyte_CI"
|
|
|
|
def __init__(self, execute=processutils.execute, *args, **kwargs):
|
|
super(QuobyteDriver, self).__init__(*args, **kwargs)
|
|
self.configuration.append_config_values(volume_opts)
|
|
|
|
# Used to manage snapshots which are currently attached to a VM.
|
|
self._nova = None
|
|
|
|
def do_setup(self, context):
|
|
"""Any initialization the volume driver does while starting."""
|
|
super(QuobyteDriver, self).do_setup(context)
|
|
|
|
self.set_nas_security_options(is_new_cinder_install=False)
|
|
self.shares = {} # address : options
|
|
self._nova = compute.API()
|
|
|
|
def check_for_setup_error(self):
|
|
if not self.configuration.quobyte_volume_url:
|
|
msg = (_("There's no Quobyte volume configured (%s). Example:"
|
|
" quobyte://<DIR host>/<volume name>") %
|
|
'quobyte_volume_url')
|
|
LOG.warning(msg)
|
|
raise exception.VolumeDriverException(msg)
|
|
|
|
# Check if mount.quobyte is installed
|
|
try:
|
|
self._execute('mount.quobyte', check_exit_code=False,
|
|
run_as_root=False)
|
|
except OSError as exc:
|
|
if exc.errno == errno.ENOENT:
|
|
raise exception.VolumeDriverException(
|
|
'mount.quobyte is not installed')
|
|
else:
|
|
raise
|
|
|
|
def set_nas_security_options(self, is_new_cinder_install):
|
|
self._execute_as_root = False
|
|
|
|
LOG.debug("nas_secure_file_* settings are %(ops)s and %(perm)s",
|
|
{'ops': self.configuration.nas_secure_file_operations,
|
|
'perm': self.configuration.nas_secure_file_permissions}
|
|
)
|
|
|
|
if self.configuration.nas_secure_file_operations == 'auto':
|
|
"""Note (kaisers): All previous Quobyte driver versions ran with
|
|
secure settings hardcoded to 'True'. Therefore the default 'auto'
|
|
setting can safely be mapped to the same, secure, setting.
|
|
"""
|
|
LOG.debug("Mapping 'auto' value to 'true' for"
|
|
" nas_secure_file_operations.")
|
|
self.configuration.nas_secure_file_operations = 'true'
|
|
|
|
if self.configuration.nas_secure_file_permissions == 'auto':
|
|
"""Note (kaisers): All previous Quobyte driver versions ran with
|
|
secure settings hardcoded to 'True'. Therefore the default 'auto'
|
|
setting can safely be mapped to the same, secure, setting.
|
|
"""
|
|
LOG.debug("Mapping 'auto' value to 'true' for"
|
|
" nas_secure_file_permissions.")
|
|
self.configuration.nas_secure_file_permissions = 'true'
|
|
|
|
if self.configuration.nas_secure_file_operations == 'false':
|
|
LOG.warning(_LW("The NAS file operations will be run as "
|
|
"root, allowing root level access at the storage "
|
|
"backend."))
|
|
self._execute_as_root = True
|
|
else:
|
|
LOG.info(_LI("The NAS file operations will be run as"
|
|
" non privileged user in secure mode. Please"
|
|
" ensure your libvirtd settings have been configured"
|
|
" accordingly (see section 'OpenStack' in the Quobyte"
|
|
" Manual."))
|
|
|
|
if self.configuration.nas_secure_file_permissions == 'false':
|
|
LOG.warning(_LW("The NAS file permissions mode will be 666 "
|
|
"(allowing other/world read & write access)."))
|
|
|
|
def _qemu_img_info(self, path, volume_name):
|
|
return super(QuobyteDriver, self)._qemu_img_info_base(
|
|
path, volume_name, self.configuration.quobyte_mount_point_base)
|
|
|
|
@utils.synchronized('quobyte', external=False)
|
|
def create_cloned_volume(self, volume, src_vref):
|
|
"""Creates a clone of the specified volume."""
|
|
self._create_cloned_volume(volume, src_vref)
|
|
|
|
@utils.synchronized('quobyte', external=False)
|
|
def create_volume(self, volume):
|
|
return super(QuobyteDriver, self).create_volume(volume)
|
|
|
|
@utils.synchronized('quobyte', external=False)
|
|
def create_volume_from_snapshot(self, volume, snapshot):
|
|
return self._create_volume_from_snapshot(volume, snapshot)
|
|
|
|
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.
|
|
"""
|
|
|
|
LOG.debug("snapshot: %(snap)s, volume: %(vol)s, ",
|
|
{'snap': snapshot.id,
|
|
'vol': volume.id,
|
|
'size': volume_size})
|
|
|
|
info_path = self._local_path_volume_info(snapshot.volume)
|
|
snap_info = self._read_info_file(info_path)
|
|
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)
|
|
|
|
path_to_new_vol = self._local_path_volume(volume)
|
|
|
|
LOG.debug("will copy from snapshot at %s", path_to_snap_img)
|
|
|
|
if self.configuration.quobyte_qcow2_volumes:
|
|
out_format = 'qcow2'
|
|
else:
|
|
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)
|
|
|
|
@utils.synchronized('quobyte', external=False)
|
|
def delete_volume(self, volume):
|
|
"""Deletes a logical volume."""
|
|
|
|
if not volume.provider_location:
|
|
LOG.warning(_LW('Volume %s does not have provider_location '
|
|
'specified, skipping'), volume.name)
|
|
return
|
|
|
|
self._ensure_share_mounted(volume.provider_location)
|
|
|
|
volume_dir = self._local_volume_dir(volume)
|
|
mounted_path = os.path.join(volume_dir,
|
|
self.get_active_image_from_info(volume))
|
|
|
|
self._execute('rm', '-f', mounted_path,
|
|
run_as_root=self._execute_as_root)
|
|
|
|
# If an exception (e.g. timeout) occurred during delete_snapshot, the
|
|
# base volume may linger around, so just delete it if it exists
|
|
base_volume_path = self._local_path_volume(volume)
|
|
fileutils.delete_if_exists(base_volume_path)
|
|
|
|
info_path = self._local_path_volume_info(volume)
|
|
fileutils.delete_if_exists(info_path)
|
|
|
|
@utils.synchronized('quobyte', external=False)
|
|
def create_snapshot(self, snapshot):
|
|
"""Apply locking to the create snapshot operation."""
|
|
|
|
return self._create_snapshot(snapshot)
|
|
|
|
@utils.synchronized('quobyte', external=False)
|
|
def delete_snapshot(self, snapshot):
|
|
"""Apply locking to the delete snapshot operation."""
|
|
self._delete_snapshot(snapshot)
|
|
|
|
@utils.synchronized('quobyte', external=False)
|
|
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/%s' % (self.configuration.quobyte_mount_point_base,
|
|
self._get_hash_str(volume.provider_location),
|
|
active_file)
|
|
|
|
data = {'export': volume.provider_location,
|
|
'name': active_file}
|
|
if volume.provider_location in self.shares:
|
|
data['options'] = self.shares[volume.provider_location]
|
|
|
|
# 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': 'quobyte',
|
|
'data': data,
|
|
'mount_point_base': self.configuration.quobyte_mount_point_base
|
|
}
|
|
|
|
@utils.synchronized('quobyte', external=False)
|
|
def copy_volume_to_image(self, context, volume, image_service, image_meta):
|
|
self._copy_volume_to_image(context, volume, image_service,
|
|
image_meta)
|
|
|
|
@utils.synchronized('quobyte', external=False)
|
|
def extend_volume(self, volume, size_gb):
|
|
volume_path = self.local_path(volume)
|
|
volume_filename = os.path.basename(volume_path)
|
|
|
|
# Ensure no snapshots exist for the volume
|
|
active_image = self.get_active_image_from_info(volume)
|
|
if volume_filename != active_image:
|
|
msg = _('Extend volume is only supported for this'
|
|
' driver when no snapshots exist.')
|
|
raise exception.InvalidVolume(msg)
|
|
|
|
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 _do_create_volume(self, volume):
|
|
"""Create a volume on given Quobyte volume.
|
|
|
|
:param volume: volume reference
|
|
"""
|
|
volume_path = self.local_path(volume)
|
|
volume_size = volume.size
|
|
|
|
if self.configuration.quobyte_qcow2_volumes:
|
|
self._create_qcow2_file(volume_path, volume_size)
|
|
else:
|
|
if self.configuration.quobyte_sparsed_volumes:
|
|
self._create_sparsed_file(volume_path, volume_size)
|
|
else:
|
|
self._create_regular_file(volume_path, volume_size)
|
|
|
|
self._set_rw_permissions_for_all(volume_path)
|
|
|
|
def _load_shares_config(self, share_file=None):
|
|
"""Put 'quobyte_volume_url' into the 'shares' list.
|
|
|
|
:param share_file: string, Not used because the user has to specify
|
|
the Quobyte volume directly.
|
|
"""
|
|
self.shares = {}
|
|
|
|
url = self.configuration.quobyte_volume_url
|
|
|
|
# Strip quobyte:// from the URL
|
|
protocol = self.driver_volume_type + "://"
|
|
if url.startswith(protocol):
|
|
url = url[len(protocol):]
|
|
|
|
self.shares[url] = None # None = No extra mount options.
|
|
|
|
LOG.debug("Quobyte Volume URL set to: %s", self.shares)
|
|
|
|
def _ensure_share_mounted(self, quobyte_volume):
|
|
"""Mount Quobyte volume.
|
|
|
|
:param quobyte_volume: string
|
|
"""
|
|
mount_path = self._get_mount_point_for_share(quobyte_volume)
|
|
self._mount_quobyte(quobyte_volume, mount_path, ensure=True)
|
|
|
|
@utils.synchronized('quobyte_ensure', external=False)
|
|
def _ensure_shares_mounted(self):
|
|
"""Mount the Quobyte volume.
|
|
|
|
Used for example by RemoteFsDriver._update_volume_stats
|
|
"""
|
|
self._mounted_shares = []
|
|
|
|
self._load_shares_config()
|
|
|
|
for share in self.shares.keys():
|
|
try:
|
|
self._ensure_share_mounted(share)
|
|
self._mounted_shares.append(share)
|
|
except Exception as exc:
|
|
LOG.warning(_LW('Exception during mounting %s'), exc)
|
|
|
|
LOG.debug('Available shares %s', self._mounted_shares)
|
|
|
|
def _find_share(self, volume_size_in_gib):
|
|
"""Returns the mounted Quobyte volume.
|
|
|
|
Multiple shares are not supported because the virtualization of
|
|
multiple storage devices is taken care of at the level of Quobyte USP.
|
|
|
|
For different types of volumes e.g., SSD vs. rotating disks, use
|
|
multiple backends in Cinder.
|
|
|
|
:param volume_size_in_gib: int size in GB. Ignored by this driver.
|
|
"""
|
|
|
|
if not self._mounted_shares:
|
|
raise exception.NotFound()
|
|
|
|
assert len(self._mounted_shares) == 1, 'There must be exactly' \
|
|
' one Quobyte volume.'
|
|
target_volume = self._mounted_shares[0]
|
|
|
|
LOG.debug('Selected %s as target Quobyte volume.', target_volume)
|
|
|
|
return target_volume
|
|
|
|
def _get_mount_point_for_share(self, quobyte_volume):
|
|
"""Return mount point for Quobyte volume.
|
|
|
|
:param quobyte_volume: Example: storage-host/openstack-volumes
|
|
"""
|
|
return os.path.join(self.configuration.quobyte_mount_point_base,
|
|
self._get_hash_str(quobyte_volume))
|
|
|
|
# open() wrapper to mock reading from /proc/mount.
|
|
@staticmethod
|
|
def read_proc_mount(): # pragma: no cover
|
|
return open('/proc/mounts')
|
|
|
|
def _mount_quobyte(self, quobyte_volume, mount_path, ensure=False):
|
|
"""Mount Quobyte volume to mount path."""
|
|
mounted = False
|
|
for l in QuobyteDriver.read_proc_mount():
|
|
if l.split()[1] == mount_path:
|
|
mounted = True
|
|
break
|
|
|
|
if mounted:
|
|
try:
|
|
os.stat(mount_path)
|
|
except OSError as exc:
|
|
if exc.errno == errno.ENOTCONN:
|
|
mounted = False
|
|
try:
|
|
LOG.info(_LI('Fixing previous mount %s which was not'
|
|
' unmounted correctly.'), mount_path)
|
|
self._execute('umount.quobyte', mount_path,
|
|
run_as_root=self._execute_as_root)
|
|
except processutils.ProcessExecutionError as exc:
|
|
LOG.warning(_LW("Failed to unmount previous mount: "
|
|
"%s"), exc)
|
|
else:
|
|
# TODO(quobyte): Extend exc analysis in here?
|
|
LOG.warning(_LW("Unknown error occurred while checking "
|
|
"mount point: %s Trying to continue."),
|
|
exc)
|
|
|
|
if not mounted:
|
|
if not os.path.isdir(mount_path):
|
|
self._execute('mkdir', '-p', mount_path)
|
|
|
|
command = ['mount.quobyte', quobyte_volume, mount_path]
|
|
if self.configuration.quobyte_client_cfg:
|
|
command.extend(['-c', self.configuration.quobyte_client_cfg])
|
|
|
|
try:
|
|
LOG.info(_LI('Mounting volume: %s ...'), quobyte_volume)
|
|
self._execute(*command, run_as_root=self._execute_as_root)
|
|
LOG.info(_LI('Mounting volume: %s succeeded'), quobyte_volume)
|
|
mounted = True
|
|
except processutils.ProcessExecutionError as exc:
|
|
if ensure and 'already mounted' in exc.stderr:
|
|
LOG.warning(_LW("%s is already mounted"), quobyte_volume)
|
|
else:
|
|
raise
|
|
|
|
if mounted:
|
|
self._validate_volume(mount_path)
|
|
|
|
def _validate_volume(self, mount_path):
|
|
"""Wraps execute calls for checking validity of a Quobyte volume"""
|
|
command = ['getfattr', "-n", "quobyte.info", mount_path]
|
|
try:
|
|
self._execute(*command, run_as_root=self._execute_as_root)
|
|
except processutils.ProcessExecutionError as exc:
|
|
msg = (_("The mount %(mount_path)s is not a valid"
|
|
" Quobyte USP volume. Error: %(exc)s")
|
|
% {'mount_path': mount_path, 'exc': exc})
|
|
raise exception.VolumeDriverException(msg)
|
|
|
|
if not os.access(mount_path, os.W_OK | os.X_OK):
|
|
LOG.warning(_LW("Volume is not writable. Please broaden the file"
|
|
" permissions. Mount: %s"), mount_path)
|