manila/manila/share/drivers/generic.py

772 lines
32 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 re
import time
from oslo.config import cfg
import six
from manila import compute
from manila import context
from manila import exception
from manila.openstack.common import excutils
from manila.openstack.common import importutils
from manila.openstack.common import log as logging
from manila.openstack.common import processutils
from manila.share import driver
from manila.share.drivers import service_instance
from manila import utils
from manila import volume
LOG = logging.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_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.generic.CIFSHelper',
'NFS=manila.share.drivers.generic.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.'),
]
CONF = cfg.CONF
CONF.register_opts(share_opts)
def ensure_server(f):
def wrap(self, *args, **kwargs):
server = kwargs.get('share_server')
context = args[0]
if not server:
# For now generic driver does not support flat networking.
# When we implement flat networking in generic driver
# we will not need share server to be passed and
# will change this logic.
raise exception.ManilaException(_('Share server not found.'))
if not server.get('backend_details'):
raise exception.ManilaException(_('Share server backend '
'details missing.'))
if not self.service_instance_manager.ensure_service_instance(
context, server['backend_details']):
raise exception.ServiceInstanceUnavailable()
return f(self, *args, **kwargs)
return wrap
class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
"""Executes commands relating to Shares."""
def __init__(self, db, *args, **kwargs):
"""Do initialization."""
super(GenericShareDriver, self).__init__(*args, **kwargs)
self.admin_context = context.get_admin_context()
self.db = db
self.configuration.append_config_values(share_opts)
self.configuration.append_config_values(service_instance.server_opts)
self._helpers = {}
self.backend_name = self.configuration.safe_get(
'share_backend_name') or "Cinder_Volumes"
self.ssh_connections = {}
def _ssh_exec(self, server, command):
connection = self.ssh_connections.get(server['instance_id'])
if not connection:
ssh_pool = utils.SSHPool(server['ip'],
22,
None,
server['username'],
server['password'],
server['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(server['ssh'])
ssh = ssh_pool.create()
self.ssh_connections[server['instance_id']] = (ssh_pool, ssh)
return processutils.ssh_execute(ssh, ' '.join(command))
def check_for_setup_error(self):
"""Returns an error if prerequisites aren't met."""
pass
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.service_instance_manager = service_instance.\
ServiceInstanceManager(self.db, self._helpers,
driver_config=self.configuration)
self.share_networks_servers = (
self.service_instance_manager.share_networks_servers)
self._setup_helpers()
def _setup_helpers(self):
"""Initializes protocol-specific NAS drivers."""
for helper_str in self.configuration.share_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)
@ensure_server
def create_share(self, context, share, share_server=None):
"""Creates share."""
server_details = share_server['backend_details']
volume = self._allocate_container(self.admin_context, share)
volume = self._attach_volume(
self.admin_context,
share,
server_details['instance_id'],
volume)
self._format_device(server_details, volume)
self._mount_device(share, server_details, volume)
location = self._get_helper(share).create_export(
server_details,
share['name'])
return location
def _format_device(self, server_details, volume):
"""Formats device attached to the service vm."""
command = ['sudo', 'mkfs.%s' % self.configuration.share_volume_fstype,
volume['mountpoint']]
self._ssh_exec(server_details, command)
def _is_device_mounted(self, share, server_details, volume=None):
"""Checks whether volume already mounted or not."""
mount_path = self._get_mount_path(share)
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 _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' % share['id'])
def _mount_device_with_lock():
mount_path = self._get_mount_path(share)
log_data = {
'dev': volume['mountpoint'],
'path': mount_path,
'server': server_details['instance_id'],
}
try:
if not self._is_device_mounted(share, 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, '&&']
mount_cmd.extend(['sudo mount', volume['mountpoint'],
mount_path])
mount_cmd.extend(['&& sudo chmod 777', mount_path])
self._ssh_exec(server_details, mount_cmd)
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()
def _unmount_device(self, share, server_details):
"""Unmounts block device from directory on service vm."""
@utils.synchronized('generic_driver_mounts_%s' % share['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(share, 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)
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'])
self.compute_api.instance_volume_attach(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-use':
return volume
elif volume['status'] != 'attaching':
raise exception.ManilaException(
_('Failed to attach volume %s') % volume['id'])
time.sleep(1)
else:
raise exception.ManilaException(
_('Volume have not been attached in %ss. Giving up') %
self.configuration.max_time_to_attach)
return do_attach(volume)
def _get_volume(self, context, share_id):
"""Finds volume, associated to the specific share."""
volume_name = self.configuration.volume_name_template % share_id
search_opts = {'display_name': volume_name}
if context.is_admin:
search_opts['all_tenants'] = True
volumes_list = self.volume_api.get_all(context, search_opts)
volume = None
if len(volumes_list) == 1:
volume = volumes_list[0]
elif len(volumes_list) > 1:
raise exception.ManilaException(_('Error. Ambiguous volumes'))
return volume
def _get_volume_snapshot(self, context, snapshot_id):
"""Finds volume snaphots, associated to the specific share snaphots."""
volume_snapshot_name = (
self.configuration.volume_snapshot_name_template % snapshot_id)
volume_snapshot_list = self.volume_api.get_all_snapshots(
context,
{'display_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:
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)]
volume = self._get_volume(context, share['id'])
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 ('available', 'error'):
break
time.sleep(1)
else:
raise exception.ManilaException(
_('Volume have not been detached in %ss. Giving up')
% self.configuration.max_time_to_attach)
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)
t = time.time()
while time.time() - t < self.configuration.max_time_to_create_volume:
if volume['status'] == 'available':
break
if volume['status'] == 'error':
raise exception.ManilaException(_('Failed to create volume'))
time.sleep(1)
volume = self.volume_api.get(context, volume['id'])
else:
raise exception.ManilaException(
_('Volume have not been created '
'in %ss. Giving up') %
self.configuration.max_time_to_create_volume)
return volume
def _deallocate_container(self, context, share):
"""Deletes cinder volume."""
volume = self._get_volume(context, share['id'])
if volume:
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 succesfully')
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 get_share_stats(self, refresh=False):
"""Get share status.
If 'refresh' is True, run update the stats first.
"""
if refresh:
self._update_share_status()
return self._stats
def _update_share_status(self):
"""Retrieve status info from share volume group."""
LOG.debug("Updating share status")
data = {}
# Note(zhiteng): These information are driver/backend specific,
# each driver may define these values in its own config options
# or fetch from driver specific configuration file.
data["share_backend_name"] = self.backend_name
data["vendor_name"] = 'Open Source'
data["driver_version"] = '1.0'
data["storage_protocol"] = 'NFS_CIFS'
data['total_capacity_gb'] = 'infinite'
data['free_capacity_gb'] = 'infinite'
data['reserved_percentage'] = (self.configuration.
reserved_share_percentage)
data['QoS_support'] = False
self._stats = data
@ensure_server
def create_share_from_snapshot(self, context, share, snapshot,
share_server=None):
"""Is called to create share from snapshot."""
volume = self._allocate_container(self.admin_context, share, snapshot)
volume = self._attach_volume(
self.admin_context, share,
share_server['backend_details']['instance_id'], volume)
self._mount_device(share, share_server['backend_details'], volume)
location = self._get_helper(share).create_export(
share_server['backend_details'],
share['name'])
return location
@ensure_server
def delete_share(self, context, share, share_server=None):
"""Deletes share."""
self._get_helper(share).remove_export(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'])
self._deallocate_container(self.admin_context, share)
@ensure_server
def create_snapshot(self, context, snapshot, share_server=None):
"""Creates a snapshot."""
volume = self._get_volume(self.admin_context, snapshot['share_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'] == 'available':
break
if volume_snapshot['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'])
else:
raise exception.ManilaException(
_('Volume snapshot have not been '
'created in %ss. Giving up') %
self.configuration.max_time_to_create_volume)
@ensure_server
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 succesfully')
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."""
volume = self._get_volume(context, share['id'])
volume = self._attach_volume(
context,
share,
share_server['backend_details']['instance_id'],
volume)
self._mount_device(share, share_server['backend_details'], volume)
self._get_helper(share).create_export(share_server['backend_details'],
share['name'],
recreate=True)
@ensure_server
def allow_access(self, context, share, access, share_server=None):
"""Allow access to the share."""
self._get_helper(share).allow_access(share_server['backend_details'],
share['name'],
access['access_type'],
access['access_to'])
@ensure_server
def deny_access(self, context, share, access, share_server=None):
"""Deny access to the share."""
self._get_helper(share).deny_access(share_server['backend_details'],
share['name'],
access['access_type'],
access['access_to'])
def _get_helper(self, share):
if share['share_proto'].startswith('NFS'):
return self._helpers['NFS']
elif share['share_proto'].startswith('CIFS'):
return self._helpers['CIFS']
else:
raise exception.InvalidShare(reason='Wrong share type')
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['server_id'],
network_info['neutron_net_id'],
network_info['neutron_subnet_id'],
)
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")
msg = "Removing share infrastructure for service instance '%s'."
LOG.debug(msg % instance_id)
try:
self.service_instance_manager.delete_service_instance(
self.admin_context,
server_details['instance_id'],
server_details['subnet_id'],
server_details['router_id']
)
except Exception as e:
LOG.warning(e)
class NASHelperBase(object):
"""Interface to work with share."""
def __init__(self, execute, ssh_execute, config_object):
self.configuration = config_object
self._execute = execute
self._ssh_exec = ssh_execute
def init_helper(self, server):
pass
def create_export(self, server, share_name, recreate=False):
"""Create new export, delete old one if exists."""
raise NotImplementedError()
def remove_export(self, server, share_name):
"""Remove export."""
raise NotImplementedError()
def allow_access(self, server, share_name, access_type, access):
"""Allow access to the host."""
raise NotImplementedError()
def deny_access(self, local_path, share_name, access_type, access,
force=False):
"""Deny access to the host."""
raise NotImplementedError()
class NFSHelper(NASHelperBase):
"""Interface to work with share."""
def create_export(self, server, share_name, recreate=False):
"""Create new export, delete old one if exists."""
return ':'.join([server['ip'],
os.path.join(
self.configuration.share_mount_path, share_name)])
def init_helper(self, server):
try:
self._ssh_exec(server, ['sudo', 'exportfs'])
except exception.ProcessExecutionError as e:
if 'command not found' in e.stderr:
raise exception.ManilaException(
_('NFS server is not installed on %s')
% server['instance_id'])
LOG.error(e.stderr)
def remove_export(self, server, share_name):
"""Remove export."""
pass
def allow_access(self, server, share_name, access_type, access):
"""Allow access to the host."""
local_path = os.path.join(self.configuration.share_mount_path,
share_name)
if access_type != 'ip':
reason = 'only ip access type allowed'
raise exception.InvalidShareAccess(reason)
# check if presents in export
out, _ = self._ssh_exec(server, ['sudo', 'exportfs'])
out = re.search(re.escape(local_path) + '[\s\n]*' + re.escape(access),
out)
if out is not None:
raise exception.ShareAccessExists(access_type=access_type,
access=access)
self._ssh_exec(server,
['sudo', 'exportfs', '-o', 'rw,no_subtree_check',
':'.join([access, local_path])])
def deny_access(self, server, share_name, access_type, access,
force=False):
"""Deny access to the host."""
local_path = os.path.join(self.configuration.share_mount_path,
share_name)
self._ssh_exec(server, ['sudo', 'exportfs', '-u',
':'.join([access, local_path])])
class CIFSHelper(NASHelperBase):
"""Manage shares in samba server by net conf tool.
Class provides functionality to operate with CIFS shares.
Samba server should be configured to use registry as configuration
backend to allow dynamically share managements.
"""
def init_helper(self, server):
# This is smoke check that we have required dependency
self._ssh_exec(server, ['sudo', 'net', 'conf', 'list'])
def create_export(self, server, share_name, recreate=False):
"""Create share at samba server."""
create_cmd = [
'sudo', 'net', 'conf', 'addshare',
share_name, self.configuration.share_mount_path,
'writeable=y', 'guest_ok=y',
]
try:
self._ssh_exec(
server, ['sudo', 'net', 'conf', 'showshare', share_name, ])
except exception.ProcessExecutionError as parent_e:
# Share does not exist, create it
try:
self._ssh_exec(server, create_cmd)
except Exception:
# If we get here, then it will be useful
# to log parent exception too.
with excutils.save_and_reraise_exception():
LOG.error(parent_e)
else:
# Share exists
if recreate:
self._ssh_exec(
server, ['sudo', 'net', 'conf', 'delshare', share_name, ])
self._ssh_exec(server, create_cmd)
else:
msg = _('Share section %s already defined.') % share_name
raise exception.ShareBackendException(msg=msg)
parameters = {
'browseable': 'yes',
'\"create mask\"': '0755',
'\"hosts deny\"': '0.0.0.0/0', # deny all by default
'\"hosts allow\"': '127.0.0.1',
'\"read only\"': 'no',
}
set_of_commands = [':', ] # : is just placeholder
for param, value in parameters.items():
# These are combined in one list to run in one process
# instead of big chain of one action calls.
set_of_commands.extend(['&&', 'sudo', 'net', 'conf', 'setparm',
share_name, param, value])
self._ssh_exec(server, set_of_commands)
return '//%s/%s' % (server['ip'], share_name)
def remove_export(self, server, share_name):
"""Remove share definition from samba server."""
try:
self._ssh_exec(
server, ['sudo', 'net', 'conf', 'delshare', share_name])
except exception.ProcessExecutionError as e:
LOG.warning(_("Caught error trying delete share: %(error)s, try"
"ing delete it forcibly.") % {'error': e.stderr})
self._ssh_exec(server, ['sudo', 'smbcontrol', 'all', 'close-share',
share_name])
def allow_access(self, server, share_name, access_type, access):
"""Add access for share."""
if access_type != 'ip':
reason = _('Only ip access type allowed.')
raise exception.InvalidShareAccess(reason=reason)
hosts = self._get_allow_hosts(server, share_name)
if access in hosts:
raise exception.ShareAccessExists(access_type=access_type,
access=access)
hosts.append(access)
self._set_allow_hosts(server, hosts, share_name)
def deny_access(self, server, share_name, access_type, access,
force=False):
"""Remove access for share."""
try:
hosts = self._get_allow_hosts(server, share_name)
if access in hosts:
# Access rule can be in error state, if so
# it can be absent in rules, hence - skip removal.
hosts.remove(access)
self._set_allow_hosts(server, hosts, share_name)
except exception.ProcessExecutionError:
if not force:
raise
def _get_allow_hosts(self, server, share_name):
(out, _) = self._ssh_exec(server, ['sudo', 'net', 'conf', 'getparm',
share_name, '\"hosts allow\"'])
return out.split()
def _set_allow_hosts(self, server, hosts, share_name):
value = "\"" + ' '.join(hosts) + "\""
self._ssh_exec(server, ['sudo', 'net', 'conf', 'setparm', share_name,
'\"hosts allow\"', value])