1067 lines
46 KiB
Python
1067 lines
46 KiB
Python
# Copyright (c) 2014 NetApp, 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.
|
|
|
|
"""Generic Driver for shares."""
|
|
|
|
import os
|
|
import time
|
|
|
|
from oslo_concurrency import processutils
|
|
from oslo_config import cfg
|
|
from oslo_log import log
|
|
from oslo_utils import importutils
|
|
from oslo_utils import units
|
|
import retrying
|
|
import six
|
|
|
|
from manila.common import constants as const
|
|
from manila import compute
|
|
from manila import context
|
|
from manila import exception
|
|
from manila.i18n import _
|
|
from manila.share import driver
|
|
from manila.share.drivers import service_instance
|
|
from manila import utils
|
|
from manila import volume
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
share_opts = [
|
|
cfg.StrOpt('smb_template_config_path',
|
|
default='$state_path/smb.conf',
|
|
help="Path to smb config."),
|
|
cfg.StrOpt('volume_name_template',
|
|
default='manila-share-%s',
|
|
help="Volume name template."),
|
|
cfg.StrOpt('volume_snapshot_name_template',
|
|
default='manila-snapshot-%s',
|
|
help="Volume snapshot name template."),
|
|
cfg.StrOpt('share_mount_path',
|
|
default='/shares',
|
|
help="Parent path in service instance where shares "
|
|
"will be mounted."),
|
|
cfg.IntOpt('max_time_to_create_volume',
|
|
default=180,
|
|
help="Maximum time to wait for creating cinder volume."),
|
|
cfg.IntOpt('max_time_to_extend_volume',
|
|
default=180,
|
|
help="Maximum time to wait for extending cinder volume."),
|
|
cfg.IntOpt('max_time_to_attach',
|
|
default=120,
|
|
help="Maximum time to wait for attaching cinder volume."),
|
|
cfg.StrOpt('service_instance_smb_config_path',
|
|
default='$share_mount_path/smb.conf',
|
|
help="Path to SMB config in service instance."),
|
|
cfg.ListOpt('share_helpers',
|
|
default=[
|
|
'CIFS=manila.share.drivers.helpers.CIFSHelperIPAccess',
|
|
'NFS=manila.share.drivers.helpers.NFSHelper',
|
|
],
|
|
help='Specify list of share export helpers.'),
|
|
cfg.StrOpt('share_volume_fstype',
|
|
default='ext4',
|
|
choices=['ext4', 'ext3'],
|
|
help='Filesystem type of the share volume.'),
|
|
cfg.StrOpt('cinder_volume_type',
|
|
help='Name or id of cinder volume type which will be used '
|
|
'for all volumes created by driver.'),
|
|
]
|
|
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(share_opts)
|
|
|
|
# NOTE(u_glide): These constants refer to the column number in the "df" output
|
|
BLOCK_DEVICE_SIZE_INDEX = 1
|
|
USED_SPACE_INDEX = 2
|
|
|
|
|
|
def ensure_server(f):
|
|
|
|
def wrap(self, context, *args, **kwargs):
|
|
server = kwargs.get('share_server')
|
|
|
|
if not self.driver_handles_share_servers:
|
|
if not server:
|
|
server = self.service_instance_manager.get_common_server()
|
|
kwargs['share_server'] = server
|
|
else:
|
|
raise exception.ManilaException(
|
|
_("Share server handling is not available. "
|
|
"But 'share_server' was provided. '%s'. "
|
|
"Share network should not be used.") % server.get('id'))
|
|
elif not server:
|
|
raise exception.ManilaException(
|
|
_("Share server handling is enabled. But 'share_server' "
|
|
"is not provided. Make sure you used 'share_network'."))
|
|
|
|
if not server.get('backend_details'):
|
|
raise exception.ManilaException(
|
|
_("Share server '%s' does not have backend details.") %
|
|
server['id'])
|
|
if not self.service_instance_manager.ensure_service_instance(
|
|
context, server['backend_details']):
|
|
raise exception.ServiceInstanceUnavailable()
|
|
|
|
return f(self, context, *args, **kwargs)
|
|
|
|
return wrap
|
|
|
|
|
|
class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
|
|
"""Executes commands relating to Shares."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Do initialization."""
|
|
super(GenericShareDriver, self).__init__(
|
|
[False, True], *args, **kwargs)
|
|
self.admin_context = context.get_admin_context()
|
|
self.configuration.append_config_values(share_opts)
|
|
self._helpers = {}
|
|
self.backend_name = self.configuration.safe_get(
|
|
'share_backend_name') or "Cinder_Volumes"
|
|
self.ssh_connections = {}
|
|
self._setup_service_instance_manager()
|
|
self.private_storage = kwargs.get('private_storage')
|
|
|
|
def _setup_service_instance_manager(self):
|
|
self.service_instance_manager = (
|
|
service_instance.ServiceInstanceManager(
|
|
driver_config=self.configuration))
|
|
|
|
def _ssh_exec(self, server, command, check_exit_code=True):
|
|
LOG.debug("_ssh_exec - server: %s, command: %s, check_exit_code: %s",
|
|
server, command, check_exit_code)
|
|
connection = self.ssh_connections.get(server['instance_id'])
|
|
ssh_conn_timeout = self.configuration.ssh_conn_timeout
|
|
if not connection:
|
|
ssh_pool = utils.SSHPool(server['ip'],
|
|
22,
|
|
ssh_conn_timeout,
|
|
server['username'],
|
|
server.get('password'),
|
|
server.get('pk_path'),
|
|
max_size=1)
|
|
ssh = ssh_pool.create()
|
|
self.ssh_connections[server['instance_id']] = (ssh_pool, ssh)
|
|
else:
|
|
ssh_pool, ssh = connection
|
|
|
|
if not ssh.get_transport().is_active():
|
|
ssh_pool.remove(ssh)
|
|
ssh = ssh_pool.create()
|
|
self.ssh_connections[server['instance_id']] = (ssh_pool, ssh)
|
|
|
|
# (aovchinnikov): ssh_execute does not behave well when passed
|
|
# parameters with spaces.
|
|
wrap = lambda token: "\"" + token + "\"" # noqa: E731
|
|
command = [wrap(tkn) if tkn.count(' ') else tkn for tkn in command]
|
|
return processutils.ssh_execute(ssh, ' '.join(command),
|
|
check_exit_code=check_exit_code)
|
|
|
|
def check_for_setup_error(self):
|
|
"""Returns an error if prerequisites aren't met."""
|
|
|
|
def do_setup(self, context):
|
|
"""Any initialization the generic driver does while starting."""
|
|
super(GenericShareDriver, self).do_setup(context)
|
|
self.compute_api = compute.API()
|
|
self.volume_api = volume.API()
|
|
self._setup_helpers()
|
|
|
|
common_sv_available = False
|
|
share_server = None
|
|
sv_fetch_retry_interval = 5
|
|
while not (common_sv_available or self.driver_handles_share_servers):
|
|
try:
|
|
# Verify availability of common server
|
|
share_server = (
|
|
self.service_instance_manager.get_common_server())
|
|
common_sv_available = self._is_share_server_active(
|
|
context, share_server)
|
|
except Exception as ex:
|
|
LOG.error(ex)
|
|
|
|
if not common_sv_available:
|
|
time.sleep(sv_fetch_retry_interval)
|
|
LOG.warning("Waiting for the common service VM to become "
|
|
"available. "
|
|
"Driver is currently uninitialized. "
|
|
"Share server: %(share_server)s "
|
|
"Retry interval: %(retry_interval)s",
|
|
dict(share_server=share_server,
|
|
retry_interval=sv_fetch_retry_interval))
|
|
|
|
def _setup_helpers(self):
|
|
"""Initializes protocol-specific NAS drivers."""
|
|
helpers = self.configuration.share_helpers
|
|
if helpers:
|
|
for helper_str in helpers:
|
|
share_proto, __, import_str = helper_str.partition('=')
|
|
helper = importutils.import_class(import_str)
|
|
self._helpers[share_proto.upper()] = helper(
|
|
self._execute,
|
|
self._ssh_exec,
|
|
self.configuration)
|
|
else:
|
|
raise exception.ManilaException(
|
|
"No protocol helpers selected for Generic Driver. "
|
|
"Please specify using config option 'share_helpers'.")
|
|
|
|
@ensure_server
|
|
def create_share(self, context, share, share_server=None):
|
|
"""Creates share."""
|
|
return self._create_share(
|
|
context, share,
|
|
snapshot=None,
|
|
share_server=share_server,
|
|
)
|
|
|
|
def _create_share(self, context, share, snapshot, share_server=None):
|
|
helper = self._get_helper(share)
|
|
server_details = share_server['backend_details']
|
|
volume = self._allocate_container(
|
|
self.admin_context, share, snapshot=snapshot)
|
|
volume = self._attach_volume(
|
|
self.admin_context, share, server_details['instance_id'], volume)
|
|
if not snapshot:
|
|
self._format_device(server_details, volume)
|
|
|
|
self._mount_device(share, server_details, volume)
|
|
export_locations = helper.create_exports(
|
|
server_details, share['name'])
|
|
return export_locations
|
|
|
|
@utils.retry(exception.ProcessExecutionError, backoff_rate=1)
|
|
def _is_device_file_available(self, server_details, volume):
|
|
"""Checks whether the device file is available"""
|
|
command = ['sudo', 'test', '-b', volume['mountpoint']]
|
|
self._ssh_exec(server_details, command)
|
|
|
|
def _format_device(self, server_details, volume):
|
|
"""Formats device attached to the service vm."""
|
|
self._is_device_file_available(server_details, volume)
|
|
command = ['sudo', 'mkfs.%s' % self.configuration.share_volume_fstype,
|
|
volume['mountpoint']]
|
|
self._ssh_exec(server_details, command)
|
|
|
|
def _is_device_mounted(self, mount_path, server_details, volume=None):
|
|
"""Checks whether volume already mounted or not."""
|
|
log_data = {
|
|
'mount_path': mount_path,
|
|
'server_id': server_details['instance_id'],
|
|
}
|
|
if volume and volume.get('mountpoint', ''):
|
|
log_data['volume_id'] = volume['id']
|
|
log_data['dev_mount_path'] = volume['mountpoint']
|
|
msg = ("Checking whether volume '%(volume_id)s' with mountpoint "
|
|
"'%(dev_mount_path)s' is mounted on mount path '%(mount_p"
|
|
"ath)s' on server '%(server_id)s' or not." % log_data)
|
|
else:
|
|
msg = ("Checking whether mount path '%(mount_path)s' exists on "
|
|
"server '%(server_id)s' or not." % log_data)
|
|
LOG.debug(msg)
|
|
mounts_list_cmd = ['sudo', 'mount']
|
|
output, __ = self._ssh_exec(server_details, mounts_list_cmd)
|
|
mounts = output.split('\n')
|
|
for mount in mounts:
|
|
mount_elements = mount.split(' ')
|
|
if (len(mount_elements) > 2 and mount_path == mount_elements[2]):
|
|
if volume:
|
|
# Mount goes with device path and mount path
|
|
if (volume.get('mountpoint', '') == mount_elements[0]):
|
|
return True
|
|
else:
|
|
# Unmount goes only by mount path
|
|
return True
|
|
return False
|
|
|
|
def _add_mount_permanently(self, share_id, server_details):
|
|
"""Add mount permanently for mounted filesystems."""
|
|
try:
|
|
self._ssh_exec(
|
|
server_details,
|
|
['grep', share_id, const.MOUNT_FILE_TEMP,
|
|
'|', 'sudo', 'tee', '-a', const.MOUNT_FILE],
|
|
)
|
|
except exception.ProcessExecutionError as e:
|
|
LOG.error("Failed to add 'Share-%(share_id)s' mount "
|
|
"permanently on server '%(instance_id)s'.",
|
|
{"share_id": share_id,
|
|
"instance_id": server_details['instance_id']})
|
|
raise exception.ShareBackendException(msg=six.text_type(e))
|
|
try:
|
|
# Remount it to avoid postponed point of failure
|
|
self._ssh_exec(server_details, ['sudo', 'mount', '-a'])
|
|
except exception.ProcessExecutionError:
|
|
LOG.error("Failed to mount all shares on server '%s'.",
|
|
server_details['instance_id'])
|
|
|
|
def _remove_mount_permanently(self, share_id, server_details):
|
|
"""Remove mount permanently from mounted filesystems."""
|
|
try:
|
|
self._ssh_exec(
|
|
server_details,
|
|
['sudo', 'sed', '-i', '\'/%s/d\'' % share_id,
|
|
const.MOUNT_FILE],
|
|
)
|
|
except exception.ProcessExecutionError as e:
|
|
LOG.error("Failed to remove 'Share-%(share_id)s' mount "
|
|
"permanently on server '%(instance_id)s'.",
|
|
{"share_id": share_id,
|
|
"instance_id": server_details['instance_id']})
|
|
raise exception.ShareBackendException(msg=six.text_type(e))
|
|
|
|
def _mount_device(self, share, server_details, volume):
|
|
"""Mounts block device to the directory on service vm.
|
|
|
|
Mounts attached and formatted block device to the directory if not
|
|
mounted yet.
|
|
"""
|
|
|
|
@utils.synchronized('generic_driver_mounts_'
|
|
'%s' % server_details['instance_id'])
|
|
def _mount_device_with_lock():
|
|
mount_path = self._get_mount_path(share)
|
|
device_path = volume['mountpoint']
|
|
log_data = {
|
|
'dev': device_path,
|
|
'path': mount_path,
|
|
'server': server_details['instance_id'],
|
|
}
|
|
try:
|
|
if not self._is_device_mounted(mount_path, server_details,
|
|
volume):
|
|
LOG.debug("Mounting '%(dev)s' to path '%(path)s' on "
|
|
"server '%(server)s'.", log_data)
|
|
mount_cmd = (
|
|
'sudo', 'mkdir', '-p', mount_path,
|
|
'&&', 'sudo', 'mount', device_path, mount_path,
|
|
'&&', 'sudo', 'chmod', '777', mount_path,
|
|
'&&', 'sudo', 'umount', mount_path,
|
|
# NOTE(vponomaryov): 'tune2fs' is required to make
|
|
# filesystem of share created from snapshot have
|
|
# unique ID, in case of LVM volumes, by default,
|
|
# it will have the same UUID as source volume one.
|
|
# 'tune2fs' command can be executed only when device
|
|
# is not mounted and also, in current case, it takes
|
|
# effect only after it was mounted. Closes #1645751
|
|
# NOTE(gouthamr): Executing tune2fs -U only works on
|
|
# a recently checked filesystem. See debian bug 857336
|
|
'&&', 'sudo', 'e2fsck', '-y', '-f', device_path,
|
|
'&&', 'sudo', 'tune2fs', '-U', 'random', device_path,
|
|
'&&', 'sudo', 'mount', device_path, mount_path,
|
|
)
|
|
self._ssh_exec(server_details, mount_cmd)
|
|
self._add_mount_permanently(share.id, server_details)
|
|
else:
|
|
LOG.warning("Mount point '%(path)s' already exists on "
|
|
"server '%(server)s'.", log_data)
|
|
except exception.ProcessExecutionError as e:
|
|
raise exception.ShareBackendException(msg=six.text_type(e))
|
|
return _mount_device_with_lock()
|
|
|
|
@utils.retry(exception.ProcessExecutionError)
|
|
def _unmount_device(self, share, server_details):
|
|
"""Unmounts block device from directory on service vm."""
|
|
|
|
@utils.synchronized('generic_driver_mounts_'
|
|
'%s' % server_details['instance_id'])
|
|
def _unmount_device_with_lock():
|
|
mount_path = self._get_mount_path(share)
|
|
log_data = {
|
|
'path': mount_path,
|
|
'server': server_details['instance_id'],
|
|
}
|
|
if self._is_device_mounted(mount_path, server_details):
|
|
LOG.debug("Unmounting path '%(path)s' on server "
|
|
"'%(server)s'.", log_data)
|
|
unmount_cmd = ['sudo', 'umount', mount_path, '&&', 'sudo',
|
|
'rmdir', mount_path]
|
|
self._ssh_exec(server_details, unmount_cmd)
|
|
self._remove_mount_permanently(share.id, server_details)
|
|
else:
|
|
LOG.warning("Mount point '%(path)s' does not exist on "
|
|
"server '%(server)s'.", log_data)
|
|
return _unmount_device_with_lock()
|
|
|
|
def _get_mount_path(self, share):
|
|
"""Returns the path to use for mount device in service vm."""
|
|
return os.path.join(self.configuration.share_mount_path, share['name'])
|
|
|
|
def _attach_volume(self, context, share, instance_id, volume):
|
|
"""Attaches cinder volume to service vm."""
|
|
@utils.synchronized(
|
|
"generic_driver_attach_detach_%s" % instance_id, external=True)
|
|
def do_attach(volume):
|
|
if volume['status'] == 'in-use':
|
|
attached_volumes = [vol.id for vol in
|
|
self.compute_api.instance_volumes_list(
|
|
self.admin_context, instance_id)]
|
|
if volume['id'] in attached_volumes:
|
|
return volume
|
|
else:
|
|
raise exception.ManilaException(
|
|
_('Volume %s is already attached to another instance')
|
|
% volume['id'])
|
|
|
|
@retrying.retry(stop_max_attempt_number=3,
|
|
wait_fixed=2000,
|
|
retry_on_exception=lambda exc: True)
|
|
def attach_volume():
|
|
self.compute_api.instance_volume_attach(
|
|
self.admin_context, instance_id, volume['id'])
|
|
|
|
attach_volume()
|
|
|
|
t = time.time()
|
|
while time.time() - t < self.configuration.max_time_to_attach:
|
|
volume = self.volume_api.get(context, volume['id'])
|
|
if volume['status'] == 'in-use':
|
|
return volume
|
|
elif volume['status'] not in ('attaching', 'reserved'):
|
|
raise exception.ManilaException(
|
|
_('Failed to attach volume %s') % volume['id'])
|
|
time.sleep(1)
|
|
else:
|
|
err_msg = {
|
|
'volume_id': volume['id'],
|
|
'max_time': self.configuration.max_time_to_attach
|
|
}
|
|
raise exception.ManilaException(
|
|
_('Volume %(volume_id)s has not been attached in '
|
|
'%(max_time)ss. Giving up.') % err_msg)
|
|
return do_attach(volume)
|
|
|
|
def _get_volume_name(self, share_id):
|
|
return self.configuration.volume_name_template % share_id
|
|
|
|
def _get_volume(self, context, share_id):
|
|
"""Finds volume, associated to the specific share."""
|
|
volume_id = self.private_storage.get(share_id, 'volume_id')
|
|
|
|
if volume_id is not None:
|
|
return self.volume_api.get(context, volume_id)
|
|
else: # Fallback to legacy method
|
|
return self._get_volume_legacy(context, share_id)
|
|
|
|
def _get_volume_legacy(self, context, share_id):
|
|
# NOTE(u_glide): this method is deprecated and will be removed in
|
|
# future versions
|
|
volume_name = self._get_volume_name(share_id)
|
|
search_opts = {'name': volume_name}
|
|
if context.is_admin:
|
|
search_opts['all_tenants'] = True
|
|
volumes_list = self.volume_api.get_all(context, search_opts)
|
|
if len(volumes_list) == 1:
|
|
return volumes_list[0]
|
|
elif len(volumes_list) > 1:
|
|
LOG.error(
|
|
"Expected only one volume in volume list with name "
|
|
"'%(name)s', but got more than one in a result - "
|
|
"'%(result)s'.", {
|
|
'name': volume_name, 'result': volumes_list})
|
|
raise exception.ManilaException(
|
|
_("Error. Ambiguous volumes for name '%s'") % volume_name)
|
|
return None
|
|
|
|
def _get_volume_snapshot(self, context, snapshot_id):
|
|
"""Find volume snapshot associated to the specific share snapshot."""
|
|
volume_snapshot_id = self.private_storage.get(
|
|
snapshot_id, 'volume_snapshot_id')
|
|
|
|
if volume_snapshot_id is not None:
|
|
return self.volume_api.get_snapshot(context, volume_snapshot_id)
|
|
else: # Fallback to legacy method
|
|
return self._get_volume_snapshot_legacy(context, snapshot_id)
|
|
|
|
def _get_volume_snapshot_legacy(self, context, snapshot_id):
|
|
# NOTE(u_glide): this method is deprecated and will be removed in
|
|
# future versions
|
|
volume_snapshot_name = (
|
|
self.configuration.volume_snapshot_name_template % snapshot_id)
|
|
volume_snapshot_list = self.volume_api.get_all_snapshots(
|
|
context, {'name': volume_snapshot_name})
|
|
volume_snapshot = None
|
|
if len(volume_snapshot_list) == 1:
|
|
volume_snapshot = volume_snapshot_list[0]
|
|
elif len(volume_snapshot_list) > 1:
|
|
LOG.error(
|
|
"Expected only one volume snapshot in list with name "
|
|
"'%(name)s', but got more than one in a result - "
|
|
"'%(result)s'.", {
|
|
'name': volume_snapshot_name,
|
|
'result': volume_snapshot_list})
|
|
raise exception.ManilaException(
|
|
_('Error. Ambiguous volume snaphots'))
|
|
return volume_snapshot
|
|
|
|
def _detach_volume(self, context, share, server_details):
|
|
"""Detaches cinder volume from service vm."""
|
|
instance_id = server_details['instance_id']
|
|
|
|
@utils.synchronized(
|
|
"generic_driver_attach_detach_%s" % instance_id, external=True)
|
|
def do_detach():
|
|
attached_volumes = [vol.id for vol in
|
|
self.compute_api.instance_volumes_list(
|
|
self.admin_context, instance_id)]
|
|
try:
|
|
volume = self._get_volume(context, share['id'])
|
|
except exception.VolumeNotFound:
|
|
LOG.warning("Volume not found for share %s. "
|
|
"Possibly already deleted.", share['id'])
|
|
volume = None
|
|
if volume and volume['id'] in attached_volumes:
|
|
self.compute_api.instance_volume_detach(
|
|
self.admin_context,
|
|
instance_id,
|
|
volume['id']
|
|
)
|
|
t = time.time()
|
|
while time.time() - t < self.configuration.max_time_to_attach:
|
|
volume = self.volume_api.get(context, volume['id'])
|
|
if volume['status'] in (const.STATUS_AVAILABLE,
|
|
const.STATUS_ERROR):
|
|
break
|
|
time.sleep(1)
|
|
else:
|
|
err_msg = {
|
|
'volume_id': volume['id'],
|
|
'max_time': self.configuration.max_time_to_attach
|
|
}
|
|
raise exception.ManilaException(
|
|
_('Volume %(volume_id)s has not been detached in '
|
|
'%(max_time)ss. Giving up.') % err_msg)
|
|
do_detach()
|
|
|
|
def _allocate_container(self, context, share, snapshot=None):
|
|
"""Creates cinder volume, associated to share by name."""
|
|
volume_snapshot = None
|
|
if snapshot:
|
|
volume_snapshot = self._get_volume_snapshot(context,
|
|
snapshot['id'])
|
|
|
|
volume = self.volume_api.create(
|
|
context,
|
|
share['size'],
|
|
self.configuration.volume_name_template % share['id'], '',
|
|
snapshot=volume_snapshot,
|
|
volume_type=self.configuration.cinder_volume_type,
|
|
availability_zone=share['availability_zone'])
|
|
|
|
self.private_storage.update(
|
|
share['id'], {'volume_id': volume['id']})
|
|
|
|
msg_error = _('Failed to create volume')
|
|
msg_timeout = (
|
|
_('Volume has not been created in %ss. Giving up') %
|
|
self.configuration.max_time_to_create_volume
|
|
)
|
|
|
|
return self._wait_for_available_volume(
|
|
volume, self.configuration.max_time_to_create_volume,
|
|
msg_error=msg_error, msg_timeout=msg_timeout
|
|
)
|
|
|
|
def _wait_for_available_volume(self, volume, timeout,
|
|
msg_error, msg_timeout,
|
|
expected_size=None):
|
|
t = time.time()
|
|
while time.time() - t < timeout:
|
|
if volume['status'] == const.STATUS_AVAILABLE:
|
|
if expected_size and volume['size'] != expected_size:
|
|
LOG.debug("The volume %(vol_id)s is available but the "
|
|
"volume size does not match the expected size. "
|
|
"A volume resize operation may be pending. "
|
|
"Expected size: %(expected_size)s, "
|
|
"Actual size: %(volume_size)s.",
|
|
dict(vol_id=volume['id'],
|
|
expected_size=expected_size,
|
|
volume_size=volume['size']))
|
|
else:
|
|
break
|
|
elif 'error' in volume['status'].lower():
|
|
raise exception.ManilaException(msg_error)
|
|
time.sleep(1)
|
|
volume = self.volume_api.get(self.admin_context, volume['id'])
|
|
else:
|
|
raise exception.ManilaException(msg_timeout)
|
|
|
|
return volume
|
|
|
|
def _deallocate_container(self, context, share):
|
|
"""Deletes cinder volume."""
|
|
try:
|
|
volume = self._get_volume(context, share['id'])
|
|
except exception.VolumeNotFound:
|
|
LOG.info("Volume not found. Already deleted?")
|
|
volume = None
|
|
if volume:
|
|
if volume['status'] == 'in-use':
|
|
raise exception.ManilaException(
|
|
_('Volume is still in use and '
|
|
'cannot be deleted now.'))
|
|
self.volume_api.delete(context, volume['id'])
|
|
t = time.time()
|
|
while (time.time() - t <
|
|
self.configuration.max_time_to_create_volume):
|
|
try:
|
|
volume = self.volume_api.get(context, volume['id'])
|
|
except exception.VolumeNotFound:
|
|
LOG.debug('Volume was deleted successfully')
|
|
break
|
|
time.sleep(1)
|
|
else:
|
|
raise exception.ManilaException(
|
|
_('Volume have not been '
|
|
'deleted in %ss. Giving up')
|
|
% self.configuration.max_time_to_create_volume)
|
|
|
|
def _update_share_stats(self):
|
|
"""Retrieve stats info from share volume group."""
|
|
data = dict(
|
|
share_backend_name=self.backend_name,
|
|
storage_protocol='NFS_CIFS',
|
|
reserved_percentage=self.configuration.reserved_share_percentage,
|
|
)
|
|
super(GenericShareDriver, self)._update_share_stats(data)
|
|
|
|
@ensure_server
|
|
def create_share_from_snapshot(self, context, share, snapshot,
|
|
share_server=None, parent_share=None):
|
|
"""Is called to create share from snapshot."""
|
|
return self._create_share(
|
|
context, share,
|
|
snapshot=snapshot,
|
|
share_server=share_server,
|
|
)
|
|
|
|
@ensure_server
|
|
def extend_share(self, share, new_size, share_server=None):
|
|
server_details = share_server['backend_details']
|
|
|
|
helper = self._get_helper(share)
|
|
helper.disable_access_for_maintenance(server_details, share['name'])
|
|
self._unmount_device(share, server_details)
|
|
volume = self._get_volume(self.admin_context, share['id'])
|
|
|
|
if int(new_size) > volume['size']:
|
|
self._detach_volume(self.admin_context, share, server_details)
|
|
volume = self._extend_volume(self.admin_context, volume, new_size)
|
|
|
|
volume = self._attach_volume(
|
|
self.admin_context,
|
|
share,
|
|
server_details['instance_id'],
|
|
volume)
|
|
|
|
self._resize_filesystem(server_details, volume)
|
|
self._mount_device(share, server_details, volume)
|
|
helper.restore_access_after_maintenance(server_details,
|
|
share['name'])
|
|
|
|
def _extend_volume(self, context, volume, new_size):
|
|
self.volume_api.extend(context, volume['id'], new_size)
|
|
|
|
msg_error = _('Failed to extend volume %s') % volume['id']
|
|
msg_timeout = (
|
|
_('Volume has not been extended in %ss. Giving up') %
|
|
self.configuration.max_time_to_extend_volume
|
|
)
|
|
return self._wait_for_available_volume(
|
|
volume, self.configuration.max_time_to_extend_volume,
|
|
msg_error=msg_error, msg_timeout=msg_timeout,
|
|
expected_size=new_size
|
|
)
|
|
|
|
@ensure_server
|
|
def shrink_share(self, share, new_size, share_server=None):
|
|
server_details = share_server['backend_details']
|
|
|
|
helper = self._get_helper(share)
|
|
export_location = share['export_locations'][0]['path']
|
|
mount_path = helper.get_share_path_by_export_location(
|
|
server_details, export_location)
|
|
|
|
consumed_space = self._get_consumed_space(mount_path, server_details)
|
|
|
|
LOG.debug("Consumed space on share: %s", consumed_space)
|
|
|
|
if consumed_space >= new_size:
|
|
raise exception.ShareShrinkingPossibleDataLoss(
|
|
share_id=share['id'])
|
|
|
|
volume = self._get_volume(self.admin_context, share['id'])
|
|
|
|
helper.disable_access_for_maintenance(server_details, share['name'])
|
|
self._unmount_device(share, server_details)
|
|
|
|
try:
|
|
self._resize_filesystem(server_details, volume, new_size=new_size)
|
|
except exception.Invalid:
|
|
raise exception.ShareShrinkingPossibleDataLoss(
|
|
share_id=share['id'])
|
|
except Exception as e:
|
|
msg = _("Cannot shrink share: %s") % six.text_type(e)
|
|
raise exception.Invalid(msg)
|
|
finally:
|
|
self._mount_device(share, server_details, volume)
|
|
helper.restore_access_after_maintenance(server_details,
|
|
share['name'])
|
|
|
|
def _resize_filesystem(self, server_details, volume, new_size=None):
|
|
"""Resize filesystem of provided volume."""
|
|
check_command = ['sudo', 'fsck', '-pf', volume['mountpoint']]
|
|
self._ssh_exec(server_details, check_command)
|
|
command = ['sudo', 'resize2fs', volume['mountpoint']]
|
|
|
|
if new_size:
|
|
command.append("%sG" % six.text_type(new_size))
|
|
|
|
try:
|
|
self._ssh_exec(server_details, command)
|
|
except processutils.ProcessExecutionError as e:
|
|
if e.stderr.find('New size smaller than minimum') != -1:
|
|
msg = (_("Invalid 'new_size' provided: %s")
|
|
% six.text_type(new_size))
|
|
raise exception.Invalid(msg)
|
|
else:
|
|
msg = _("Cannot resize file-system: %s") % six.text_type(e)
|
|
raise exception.ManilaException(msg)
|
|
|
|
def _is_share_server_active(self, context, share_server):
|
|
"""Check if the share server is active."""
|
|
has_active_share_server = (
|
|
share_server and share_server.get('backend_details') and
|
|
self.service_instance_manager.ensure_service_instance(
|
|
context, share_server['backend_details']))
|
|
return has_active_share_server
|
|
|
|
def delete_share(self, context, share, share_server=None):
|
|
"""Deletes share."""
|
|
helper = self._get_helper(share)
|
|
if not self.driver_handles_share_servers:
|
|
share_server = self.service_instance_manager.get_common_server()
|
|
if self._is_share_server_active(context, share_server):
|
|
helper.remove_exports(
|
|
share_server['backend_details'], share['name'])
|
|
self._unmount_device(share, share_server['backend_details'])
|
|
self._detach_volume(self.admin_context, share,
|
|
share_server['backend_details'])
|
|
|
|
# Note(jun): It is an intended breakage to deal with the cases
|
|
# with any reason that caused absence of Nova instances.
|
|
self._deallocate_container(self.admin_context, share)
|
|
|
|
self.private_storage.delete(share['id'])
|
|
|
|
def create_snapshot(self, context, snapshot, share_server=None):
|
|
"""Creates a snapshot."""
|
|
model_update = {}
|
|
volume = self._get_volume(
|
|
self.admin_context, snapshot['share_instance_id'])
|
|
volume_snapshot_name = (self.configuration.
|
|
volume_snapshot_name_template % snapshot['id'])
|
|
volume_snapshot = self.volume_api.create_snapshot_force(
|
|
self.admin_context, volume['id'], volume_snapshot_name, '')
|
|
t = time.time()
|
|
while time.time() - t < self.configuration.max_time_to_create_volume:
|
|
if volume_snapshot['status'] == const.STATUS_AVAILABLE:
|
|
break
|
|
if volume_snapshot['status'] == const.STATUS_ERROR:
|
|
raise exception.ManilaException(_('Failed to create volume '
|
|
'snapshot'))
|
|
time.sleep(1)
|
|
volume_snapshot = self.volume_api.get_snapshot(
|
|
self.admin_context,
|
|
volume_snapshot['id'])
|
|
|
|
# NOTE(xyang): We should look at whether we still need to save
|
|
# volume_snapshot_id in private_storage later, now that is saved
|
|
# in provider_location.
|
|
self.private_storage.update(
|
|
snapshot['id'], {'volume_snapshot_id': volume_snapshot['id']})
|
|
# NOTE(xyang): Need to update provider_location in the db so
|
|
# that it can be used in manage/unmanage snapshot tempest tests.
|
|
model_update['provider_location'] = volume_snapshot['id']
|
|
else:
|
|
raise exception.ManilaException(
|
|
_('Volume snapshot have not been '
|
|
'created in %ss. Giving up') %
|
|
self.configuration.max_time_to_create_volume)
|
|
|
|
return model_update
|
|
|
|
def delete_snapshot(self, context, snapshot, share_server=None):
|
|
"""Deletes a snapshot."""
|
|
volume_snapshot = self._get_volume_snapshot(self.admin_context,
|
|
snapshot['id'])
|
|
if volume_snapshot is None:
|
|
return
|
|
self.volume_api.delete_snapshot(self.admin_context,
|
|
volume_snapshot['id'])
|
|
t = time.time()
|
|
while time.time() - t < self.configuration.max_time_to_create_volume:
|
|
try:
|
|
snapshot = self.volume_api.get_snapshot(self.admin_context,
|
|
volume_snapshot['id'])
|
|
except exception.VolumeSnapshotNotFound:
|
|
LOG.debug('Volume snapshot was deleted successfully')
|
|
self.private_storage.delete(snapshot['id'])
|
|
break
|
|
time.sleep(1)
|
|
else:
|
|
raise exception.ManilaException(
|
|
_('Volume snapshot have not been '
|
|
'deleted in %ss. Giving up') %
|
|
self.configuration.max_time_to_create_volume)
|
|
|
|
@ensure_server
|
|
def ensure_share(self, context, share, share_server=None):
|
|
"""Ensure that storage are mounted and exported."""
|
|
helper = self._get_helper(share)
|
|
volume = self._get_volume(context, share['id'])
|
|
|
|
# NOTE(vponomaryov): volume can be None for managed shares
|
|
if volume:
|
|
volume = self._attach_volume(
|
|
context,
|
|
share,
|
|
share_server['backend_details']['instance_id'],
|
|
volume)
|
|
self._mount_device(share, share_server['backend_details'], volume)
|
|
helper.create_exports(
|
|
share_server['backend_details'], share['name'], recreate=True)
|
|
|
|
@ensure_server
|
|
def update_access(self, context, share, access_rules, add_rules,
|
|
delete_rules, share_server=None):
|
|
"""Update access rules for given share.
|
|
|
|
This driver has two different behaviors according to parameters:
|
|
1. Recovery after error - 'access_rules' contains all access_rules,
|
|
'add_rules' and 'delete_rules' shall be empty. Previously existing
|
|
access rules are cleared and then added back according
|
|
to 'access_rules'.
|
|
|
|
2. Adding/Deleting of several access rules - 'access_rules' contains
|
|
all access_rules, 'add_rules' and 'delete_rules' contain rules which
|
|
should be added/deleted. Rules in 'access_rules' are ignored and
|
|
only rules from 'add_rules' and 'delete_rules' are applied.
|
|
|
|
:param context: Current context
|
|
:param share: Share model with share data.
|
|
:param access_rules: All access rules for given share
|
|
:param add_rules: Empty List or List of access rules which should be
|
|
added. access_rules already contains these rules.
|
|
:param delete_rules: Empty List or List of access rules which should be
|
|
removed. access_rules doesn't contain these rules.
|
|
:param share_server: None or Share server model
|
|
"""
|
|
self._get_helper(share).update_access(share_server['backend_details'],
|
|
share['name'], access_rules,
|
|
add_rules=add_rules,
|
|
delete_rules=delete_rules)
|
|
|
|
def _get_helper(self, share):
|
|
helper = self._helpers.get(share['share_proto'])
|
|
if helper:
|
|
return helper
|
|
else:
|
|
raise exception.InvalidShare(
|
|
reason="Wrong, unsupported or disabled protocol")
|
|
|
|
def get_network_allocations_number(self):
|
|
"""Get number of network interfaces to be created."""
|
|
# NOTE(vponomaryov): Generic driver does not need allocations, because
|
|
# Nova will handle it. It is valid for all multitenant drivers, that
|
|
# use service instance provided by Nova.
|
|
return 0
|
|
|
|
def _setup_server(self, network_info, metadata=None):
|
|
msg = "Creating share server '%s'."
|
|
LOG.debug(msg, network_info['server_id'])
|
|
server = self.service_instance_manager.set_up_service_instance(
|
|
self.admin_context, network_info)
|
|
for helper in self._helpers.values():
|
|
helper.init_helper(server)
|
|
return server
|
|
|
|
def _teardown_server(self, server_details, security_services=None):
|
|
instance_id = server_details.get("instance_id")
|
|
LOG.debug("Removing share infrastructure for service instance '%s'.",
|
|
instance_id)
|
|
self.service_instance_manager.delete_service_instance(
|
|
self.admin_context, server_details)
|
|
|
|
def manage_existing(self, share, driver_options):
|
|
"""Manage existing share to manila.
|
|
|
|
Generic driver accepts only one driver_option 'volume_id'.
|
|
If an administrator provides this option, then appropriate Cinder
|
|
volume will be managed by Manila as well.
|
|
|
|
:param share: share data
|
|
:param driver_options: Empty dict or dict with 'volume_id' option.
|
|
:return: dict with share size, example: {'size': 1}
|
|
"""
|
|
helper = self._get_helper(share)
|
|
share_server = self.service_instance_manager.get_common_server()
|
|
server_details = share_server['backend_details']
|
|
|
|
old_export_location = share['export_locations'][0]['path']
|
|
mount_path = helper.get_share_path_by_export_location(
|
|
share_server['backend_details'], old_export_location)
|
|
LOG.debug("Manage: mount path = %s", mount_path)
|
|
|
|
mounted = self._is_device_mounted(mount_path, server_details)
|
|
LOG.debug("Manage: is share mounted = %s", mounted)
|
|
|
|
if not mounted:
|
|
msg = _("Provided share %s is not mounted.") % share['id']
|
|
raise exception.ManageInvalidShare(reason=msg)
|
|
|
|
def get_volume():
|
|
if 'volume_id' in driver_options:
|
|
try:
|
|
return self.volume_api.get(
|
|
self.admin_context, driver_options['volume_id'])
|
|
except exception.VolumeNotFound as e:
|
|
raise exception.ManageInvalidShare(reason=six.text_type(e))
|
|
|
|
# NOTE(vponomaryov): Manila can only combine volume name by itself,
|
|
# nowhere to get volume ID from. Return None since Cinder volume
|
|
# names are not unique or fixed, hence, they can not be used for
|
|
# sure.
|
|
return None
|
|
|
|
share_volume = get_volume()
|
|
|
|
if share_volume:
|
|
instance_volumes = self.compute_api.instance_volumes_list(
|
|
self.admin_context, server_details['instance_id'])
|
|
|
|
attached_volumes = [vol.id for vol in instance_volumes]
|
|
LOG.debug('Manage: attached volumes = %s',
|
|
six.text_type(attached_volumes))
|
|
|
|
if share_volume['id'] not in attached_volumes:
|
|
msg = _("Provided volume %s is not attached "
|
|
"to service instance.") % share_volume['id']
|
|
raise exception.ManageInvalidShare(reason=msg)
|
|
|
|
linked_volume_name = self._get_volume_name(share['id'])
|
|
if share_volume['name'] != linked_volume_name:
|
|
LOG.debug('Manage: volume_id = %s', share_volume['id'])
|
|
self.volume_api.update(self.admin_context, share_volume['id'],
|
|
{'name': linked_volume_name})
|
|
|
|
self.private_storage.update(
|
|
share['id'], {'volume_id': share_volume['id']})
|
|
|
|
share_size = share_volume['size']
|
|
else:
|
|
share_size = self._get_mounted_share_size(
|
|
mount_path, share_server['backend_details'])
|
|
|
|
export_locations = helper.get_exports_for_share(
|
|
server_details, old_export_location)
|
|
return {'size': share_size, 'export_locations': export_locations}
|
|
|
|
def manage_existing_snapshot(self, snapshot, driver_options):
|
|
"""Manage existing share snapshot with manila.
|
|
|
|
:param snapshot: Snapshot data
|
|
:param driver_options: Not used by the Generic driver currently
|
|
:return: dict with share snapshot size, example: {'size': 1}
|
|
"""
|
|
model_update = {}
|
|
volume_snapshot = None
|
|
snapshot_size = snapshot.get('share_size', 0)
|
|
provider_location = snapshot.get('provider_location')
|
|
try:
|
|
volume_snapshot = self.volume_api.get_snapshot(
|
|
self.admin_context,
|
|
provider_location)
|
|
except exception.VolumeSnapshotNotFound as e:
|
|
raise exception.ManageInvalidShareSnapshot(
|
|
reason=six.text_type(e))
|
|
|
|
if volume_snapshot:
|
|
snapshot_size = volume_snapshot['size']
|
|
# NOTE(xyang): volume_snapshot_id is saved in private_storage
|
|
# in create_snapshot, so saving it here too for consistency.
|
|
# We should look at whether we still need to save it in
|
|
# private_storage later.
|
|
self.private_storage.update(
|
|
snapshot['id'], {'volume_snapshot_id': volume_snapshot['id']})
|
|
# NOTE(xyang): provider_location is used to map a Manila snapshot
|
|
# to its name on the storage backend and prevent managing of the
|
|
# same snapshot twice.
|
|
model_update['provider_location'] = volume_snapshot['id']
|
|
|
|
model_update['size'] = snapshot_size
|
|
return model_update
|
|
|
|
def unmanage_snapshot(self, snapshot):
|
|
"""Unmanage share snapshot with manila."""
|
|
|
|
self.private_storage.delete(snapshot['id'])
|
|
|
|
def _get_mount_stats_by_index(self, mount_path, server_details, index,
|
|
block_size='G'):
|
|
"""Get mount stats using df shell command.
|
|
|
|
:param mount_path: Share path on share server
|
|
:param server_details: Share server connection details
|
|
:param index: Data index in df command output:
|
|
BLOCK_DEVICE_SIZE_INDEX - Size of block device
|
|
USED_SPACE_INDEX - Used space
|
|
:param block_size: size of block (example: G, M, Mib, etc)
|
|
:returns: value of provided index
|
|
"""
|
|
share_size_cmd = ['df', '-PB%s' % block_size, mount_path]
|
|
output, __ = self._ssh_exec(server_details, share_size_cmd)
|
|
lines = output.split('\n')
|
|
return int(lines[1].split()[index][:-1])
|
|
|
|
def _get_mounted_share_size(self, mount_path, server_details):
|
|
try:
|
|
size = self._get_mount_stats_by_index(
|
|
mount_path, server_details, BLOCK_DEVICE_SIZE_INDEX)
|
|
except Exception as e:
|
|
msg = _("Cannot calculate size of share %(path)s : %(error)s") % {
|
|
'path': mount_path,
|
|
'error': six.text_type(e)
|
|
}
|
|
raise exception.ManageInvalidShare(reason=msg)
|
|
|
|
return size
|
|
|
|
def _get_consumed_space(self, mount_path, server_details):
|
|
try:
|
|
size = self._get_mount_stats_by_index(
|
|
mount_path, server_details, USED_SPACE_INDEX, block_size='M')
|
|
size /= float(units.Ki)
|
|
except Exception as e:
|
|
msg = _("Cannot calculate consumed space on share "
|
|
"%(path)s : %(error)s") % {
|
|
'path': mount_path,
|
|
'error': six.text_type(e)
|
|
}
|
|
raise exception.InvalidShare(reason=msg)
|
|
|
|
return size
|