Add LVM driver

Reuse code of old LVM share driver.

LVM Driver is 1st party Manila driver with NFS and CIFS support
and no share server support.

LVM Driver doesn't rely on Neutron, Nova, or Cinder. As such, it
is ideal for testing purposes and, after a period of maturation,
it should be useful in production environments.

Move generic driver's helpers to manila/share/drivers/helpers.py
and reuse them in LVM driver.

Implement 'ro' access, user access, extend_share function for
LVM driver.

Implements bp lvm-driver

Change-Id: Ia46c51ed400dbb0f1d87a758fb165ca711ed3d7c
This commit is contained in:
Julia Varlamova 2016-01-14 08:02:34 -05:00 committed by Ben Swartzlander
parent 83c4e1521c
commit 401c8d982e
14 changed files with 2252 additions and 824 deletions

View File

@ -61,13 +61,11 @@ iniset $TEMPEST_CONFIG share share_creation_retry_number 2
SUPPRESS_ERRORS=${SUPPRESS_ERRORS_IN_CLEANUP:-True}
iniset $TEMPEST_CONFIG share suppress_errors_in_cleanup $SUPPRESS_ERRORS
# Enable consistency group tests
RUN_MANILA_CG_TESTS=${RUN_MANILA_CG_TESTS:-True}
iniset $TEMPEST_CONFIG share run_consistency_group_tests $RUN_MANILA_CG_TESTS
USERNAME_FOR_USER_RULES=${USERNAME_FOR_USER_RULES:-"manila"}
PASSWORD_FOR_SAMBA_USER=${PASSWORD_FOR_SAMBA_USER:-$USERNAME_FOR_USER_RULES}
# Enable manage/unmanage tests
RUN_MANILA_CG_TESTS=${RUN_MANILA_CG_TESTS:-True}
RUN_MANILA_MANAGE_TESTS=${RUN_MANILA_MANAGE_TESTS:-True}
iniset $TEMPEST_CONFIG share run_manage_unmanage_tests $RUN_MANILA_MANAGE_TESTS
MANILA_CONF=${MANILA_CONF:-/etc/manila/manila.conf}
@ -141,6 +139,32 @@ elif [[ "$DRIVER" == "generic" ]]; then
fi
fi
if [[ "$DRIVER" == "lvm" ]]; then
MANILA_TEMPEST_CONCURRENCY=8
RUN_MANILA_CG_TESTS=False
RUN_MANILA_MANAGE_TESTS=False
iniset $TEMPEST_CONFIG share run_shrink_tests False
iniset $TEMPEST_CONFIG share run_migration_tests False
iniset $TEMPEST_CONFIG share enable_ip_rules_for_protocols 'nfs'
iniset $TEMPEST_CONFIG share enable_user_rules_for_protocols 'cifs'
if ! grep $USERNAME_FOR_USER_RULES "/etc/passwd"; then
sudo useradd $USERNAME_FOR_USER_RULES
fi
(echo $PASSWORD_FOR_SAMBA_USER; echo $PASSWORD_FOR_SAMBA_USER) | sudo smbpasswd -s -a $USERNAME_FOR_USER_RULES
sudo smbpasswd -e $USERNAME_FOR_USER_RULES
samba_daemon_name=smbd
if is_fedora; then
samba_daemon_name=smb
fi
sudo service $samba_daemon_name restart
fi
# Enable consistency group tests
iniset $TEMPEST_CONFIG share run_consistency_group_tests $RUN_MANILA_CG_TESTS
# Enable manage/unmanage tests
iniset $TEMPEST_CONFIG share run_manage_unmanage_tests $RUN_MANILA_MANAGE_TESTS
# Also, we should wait until service VM is available
# before running Tempest tests using Generic driver in DHSS=False mode.
source $BASE/new/manila/contrib/ci/common.sh

View File

@ -57,6 +57,11 @@ else
echo "MANILA_MULTI_BACKEND=False" >> $localrc_path
fi
if [[ "$DRIVER" == "lvm" ]]; then
echo "SHARE_DRIVER=manila.share.drivers.lvm.LVMShareDriver" >> $localrc_path
echo "SHARE_BACKING_FILE_SIZE=32000M" >> $localrc_path
fi
# Enabling isolated metadata in Neutron is required because
# Tempest creates isolated networks and created vm's in scenario tests don't
# have access to Nova Metadata service. This leads to unavailability of

View File

@ -8,12 +8,42 @@ set -o xtrace
# Entry Points
# ------------
function _clean_share_group {
local vg=$1
local vg_prefix=$2
# Clean out existing shares
for lv in `sudo lvs --noheadings -o lv_name $vg`; do
# vg_prefix prefixes the LVs we want
if [[ "${lv#$vg_prefix}" != "$lv" ]]; then
sudo umount -f $MANILA_MNT_DIR/$lv
sudo lvremove -f $vg/$lv
sudo rm -rf $MANILA_MNT_DIR/$lv
fi
done
}
function _clean_manila_lvm_backing_file {
local vg=$1
# if there is no logical volume left, it's safe to attempt a cleanup
# of the backing file
if [ -z "`sudo lvs --noheadings -o lv_name $vg`" ]; then
# if the backing physical device is a loop device, it was probably setup by devstack
VG_DEV=$(sudo losetup -j $DATA_DIR/${vg}-backing-file | awk -F':' '/backing-file/ { print $1
}')
if [[ -n "$VG_DEV" ]]; then
sudo losetup -d $VG_DEV
rm -f $DATA_DIR/${vg}-backing-file
fi
fi
}
# cleanup_manila - Remove residual data files, anything left over from previous
# runs that a clean run would need to clean up
function cleanup_manila {
# This is placeholder.
# All stuff, that are created by Generic driver will be cleaned up by other services.
:
# All stuff, that are created by share drivers will be cleaned up by other services.
_clean_share_group $SHARE_GROUP $SHARE_NAME_PREFIX
_clean_manila_lvm_backing_file $SHARE_GROUP
}
# configure_default_backends - configures default Manila backends with generic driver.
@ -151,6 +181,8 @@ function configure_manila {
iniset $MANILA_CONF DEFAULT wsgi_keep_alive False
iniset $MANILA_CONF DEFAULT lvm_share_volume_group $SHARE_GROUP
# Note: set up config group does not mean that this backend will be enabled.
# To enable it, specify its name explicitly using "enabled_share_backends" opt.
configure_default_backends
@ -363,6 +395,32 @@ function init_manila {
$MANILA_BIN_DIR/manila-manage db version
fi
if [ "$SHARE_DRIVER" == "manila.share.drivers.lvm.LVMShareDriver" ]; then
if is_service_enabled m-shr; then
# Configure a default volume group called '`lvm-shares`' for the share
# service if it does not yet exist. If you don't wish to use a file backed
# volume group, create your own volume group called ``stack-volumes`` before
# invoking ``stack.sh``.
#
# By default, the backing file is 8G in size, and is stored in ``/opt/stack/data``.
if ! sudo vgs $SHARE_GROUP; then
if [ "$CONFIGURE_BACKING_FILE" = "True" ]; then
SHARE_BACKING_FILE=${SHARE_BACKING_FILE:-$DATA_DIR/${SHARE_GROUP}-backing-file}
# Only create if the file doesn't already exists
[[ -f $SHARE_BACKING_FILE ]] || truncate -s $SHARE_BACKING_FILE_SIZE $SHARE_BACKING_FILE
DEV=`sudo losetup -f --show $SHARE_BACKING_FILE`
else
DEV=$SHARE_BACKING_FILE
fi
# Only create if the loopback device doesn't contain $SHARE_GROUP
if ! sudo vgs $SHARE_GROUP; then sudo vgcreate $SHARE_GROUP $DEV; fi
fi
mkdir -p $MANILA_STATE_PATH/shares
fi
fi
# Create cache dir
sudo mkdir -p $MANILA_AUTH_CACHE_DIR
sudo chown $STACK_USER $MANILA_AUTH_CACHE_DIR
@ -373,12 +431,49 @@ function init_manila {
function install_manila {
git_clone $MANILACLIENT_REPO $MANILACLIENT_DIR $MANILACLIENT_BRANCH
if [ "$SHARE_DRIVER" == "manila.share.drivers.lvm.LVMShareDriver" ]; then
if is_service_enabled m-shr; then
if is_ubuntu; then
sudo apt-get install -y nfs-kernel-server nfs-common samba
elif is_fedora; then
sudo yum install -y nfs-utils nfs-utils-lib samba
fi
fi
fi
# install manila-ui if horizon is enabled
if is_service_enabled horizon && [ "$MANILA_UI_ENABLED" = "True" ]; then
git_clone $MANILA_UI_REPO $MANILA_UI_DIR $MANILA_UI_BRANCH
fi
}
#configure_samba - Configure node as Samba server
function configure_samba {
if [ "$SHARE_DRIVER" == "manila.share.drivers.lvm.LVMShareDriver" ]; then
samba_daemon_name=smbd
if is_service_enabled m-shr; then
if is_fedora; then
samba_daemon_name=smb
fi
sudo service $samba_daemon_name restart || echo "Couldn't restart '$samba_daemon_name' service"
fi
sudo cp /usr/share/samba/smb.conf $SMB_CONF
sudo chown stack -R /etc/samba
iniset $SMB_CONF global include registry
iniset $SMB_CONF global security user
if [ ! -d "$SMB_PRIVATE_DIR" ]; then
sudo mkdir $SMB_PRIVATE_DIR
sudo touch $SMB_PRIVATE_DIR/secrets.tdb
fi
for backend_name in ${MANILA_ENABLED_BACKENDS//,/ }; do
iniset $MANILA_CONF $backend_name driver_handles_share_servers False
iniset $MANILA_CONF $backend_name lvm_share_export_ip $MANILA_SERVICE_HOST
done
fi
}
# start_manila - Start running processes, including screen
function start_manila {
# restart apache to reload running horizon if manila-ui is enabled
@ -468,6 +563,9 @@ elif [[ "$1" == "stack" && "$2" == "extra" ]]; then
backends for which handlng of share servers is disabled."
create_service_share_servers
echo_summary "Configure Samba server"
configure_samba
echo_summary "Starting Manila"
start_manila

View File

@ -127,6 +127,13 @@ MANILA_SHARE_BACKEND1_NAME=${MANILA_SHARE_BACKEND1_NAME:-GENERIC1} # deprecated
MANILA_BACKEND2_CONFIG_GROUP_NAME=${MANILA_BACKEND2_CONFIG_GROUP_NAME:-generic2} # deprecated
MANILA_SHARE_BACKEND2_NAME=${MANILA_SHARE_BACKEND2_NAME:-GENERIC2} # deprecated
# Options for configuration of LVM share driver
SHARE_BACKING_FILE_SIZE=${SHARE_BACKING_FILE_SIZE:-8400M}
SHARE_GROUP=${SHARE_GROUP:-lvm-shares}
MANILA_MNT_DIR=${MANILA_MNT_DIR:=$MANILA_STATE_PATH/mnt}
SMB_CONF=${SMB_CONF:-/etc/samba/smb.conf}
SMB_PRIVATE_DIR=${SMB_PRIVATE_DIR:-/var/lib/samba/private}
CONFIGURE_BACKING_FILE=${CONFIGURE_BACKING_FILE:-"True"}
# Enable manila services
# ----------------------

View File

@ -55,6 +55,8 @@ Mapping of share drivers and share features support
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| IBM GPFS | DHSS = False(K) | \- | L | \- | K | K |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| LVM | DHSS = False (M) | \- | M | \- | M | M |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| Quobyte | DHSS = False (K) | \- | M | M | \- | \- |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| Windows SMB | DHSS = True (L) & False (L) | L | L | L | L | L |
@ -94,6 +96,8 @@ Mapping of share drivers and share access rules support
+----------------------------------------+--------------+----------------+------------+--------------+----------------+------------+
| Huawei | NFS (K) |NFS (M),CIFS (K)| \- | NFS (K) |NFS (M),CIFS (K)| \- |
+----------------------------------------+--------------+----------------+------------+--------------+----------------+------------+
| LVM | NFS (M) | CIFS (M) | \- | NFS (M) | CIFS (M) | \- |
+----------------------------------------+--------------+----------------+------------+--------------+----------------+------------+
| Quobyte | NFS (K) | \- | \- | NFS (K) | \- | \- |
+----------------------------------------+--------------+----------------+------------+--------------+----------------+------------+
| Windows SMB | \- | CIFS (L) | \- | \- | CIFS (L) | \- |
@ -129,6 +133,8 @@ Mapping of share drivers and security services support
+----------------------------------------+------------------+-----------------+------------------+
| Huawei | M | M | \- |
+----------------------------------------+------------------+-----------------+------------------+
| LVM | \- | \- | \- |
+----------------------------------------+------------------+-----------------+------------------+
| Quobyte | \- | \- | \- |
+----------------------------------------+------------------+-----------------+------------------+
| Windows SMB | L | \- | \- |

View File

@ -7,6 +7,49 @@ chown: CommandFilter, chown, root
# manila/utils.py : 'cat', '%s'
cat: CommandFilter, cat, root
# manila/share/drivers/lvm.py: 'mkfs.ext4', '/dev/mapper/%s'
mkfs.ext4: CommandFilter, /sbin/mkfs.ext4, root
# manila/share/drivers/lvm.py: 'mkfs.ext3', '/dev/mapper/%s'
mkfs.ext3: CommandFilter, /sbin/mkfs.ext3, root
# manila/share/drivers/lvm.py: 'smbd', '-s', '%s', '-D'
smbd: CommandFilter, /usr/sbin/smbd, root
smb: CommandFilter, /usr/sbin/smb, root
# manila/share/drivers/lvm.py: 'rmdir', '%s'
rmdir: CommandFilter, /bin/rmdir, root
# manila/share/drivers/lvm.py: 'dd' 'count=0', 'if=%s' % srcstr, 'of=%s'
dd: CommandFilter, dd, root
# manila/share/drivers/lvm.py: 'fsck', '-pf', %s
fsck: CommandFilter, fsck, root
# manila/share/drivers/lvm.py: 'resize2fs', %s
resize2fs: CommandFilter, resize2fs, root
# manila/share/drivers/helpers.py: 'smbcontrol', 'all', 'close-share', '%s'
smbcontrol: CommandFilter, /usr/bin/smbcontrol, root
# manila/share/drivers/helpers.py: 'net', 'conf', 'addshare', '%s', '%s', 'writeable=y', 'guest_ok=y
# manila/share/drivers/helpers.py: 'net', 'conf', 'delshare', '%s'
# manila/share/drivers/helpers.py: 'net', 'conf', 'setparm', '%s', '%s', '%s'
# manila/share/drivers/helpers.py: 'net', 'conf', 'getparm', '%s', 'hosts allow'
net: CommandFilter, /usr/bin/net, root
# manila/share/drivers/lvm.py: 'lvremove', '-f', "%s/%s
lvremove: CommandFilter, lvremove, root
# manila/share/drivers/lvm.py: 'lvextend', '-L', '%sG''-n', %s
lvextend: CommandFilter, lvextend, root
# manila/share/drivers/lvm.py: 'lvcreate', '-L', %s, '-n', %s
lvcreate: CommandFilter, lvcreate, root
# manila/share/drivers/lvm.py: 'vgs', %s, '--rows'
vgs: CommandFilter, vgs, root
# manila/share/drivers/glusterfs.py: 'mkdir', '%s'
# manila/share/drivers/ganesha/manager.py: 'mkdir', '-p', '%s'
mkdir: CommandFilter, mkdir, root

View File

@ -16,13 +16,11 @@
"""Generic Driver for shares."""
import os
import re
import time
from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_log import log
from oslo_utils import excutils
from oslo_utils import importutils
from oslo_utils import units
import retrying
@ -71,8 +69,8 @@ share_opts = [
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',
'CIFS=manila.share.drivers.helpers.CIFSHelperIPAccess',
'NFS=manila.share.drivers.helpers.NFSHelper',
],
help='Specify list of share export helpers.'),
cfg.StrOpt('share_volume_fstype',
@ -1132,331 +1130,3 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
clone_list.append(clone_info)
return clone_list
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_level,
access_to):
"""Allow access to the host."""
raise NotImplementedError()
def deny_access(self, server, share_name, access, force=False):
"""Deny access to the host."""
raise NotImplementedError()
@staticmethod
def _verify_server_has_public_address(server):
if 'public_address' not in server:
raise exception.ManilaException(
_("Can not get 'public_address' for generation of export."))
def get_exports_for_share(self, server, old_export_location):
"""Returns list of exports based on server info."""
raise NotImplementedError()
def get_share_path_by_export_location(self, server, export_location):
"""Returns share path by its export location."""
raise NotImplementedError()
def disable_access_for_maintenance(self, server, share_name):
"""Disables access to share to perform maintenance operations."""
def restore_access_after_maintenance(self, server, share_name):
"""Enables access to share after maintenance operations were done."""
def _get_maintenance_file_path(self, share_name):
return os.path.join(self.configuration.share_mount_path,
"%s.maintenance" % share_name)
def nfs_synchronized(f):
def wrapped_func(self, *args, **kwargs):
key = "nfs-%s" % args[0]["instance_id"]
@utils.synchronized(key)
def source_func(self, *args, **kwargs):
return f(self, *args, **kwargs)
return source_func(self, *args, **kwargs)
return wrapped_func
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['public_address'],
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
@nfs_synchronized
def allow_access(self, server, share_name, access_type, access_level,
access_to):
"""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_to), out)
if out is not None:
raise exception.ShareAccessExists(access_type=access_type,
access=access_to)
self._ssh_exec(
server,
['sudo', 'exportfs', '-o', '%s,no_subtree_check' % access_level,
':'.join([access_to, local_path])])
self._sync_nfs_temp_and_perm_files(server)
@nfs_synchronized
def deny_access(self, server, share_name, 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['access_to'], local_path])])
self._sync_nfs_temp_and_perm_files(server)
def _sync_nfs_temp_and_perm_files(self, server):
"""Sync changes of exports with permanent NFS config file.
This is required to ensure, that after share server reboot, exports
still exist.
"""
sync_cmd = [
'sudo', 'cp ', const.NFS_EXPORTS_FILE_TEMP, const.NFS_EXPORTS_FILE,
'&&',
'sudo', 'exportfs', '-a',
]
self._ssh_exec(server, sync_cmd)
def get_exports_for_share(self, server, old_export_location):
self._verify_server_has_public_address(server)
path = old_export_location.split(':')[-1]
return [':'.join([server['public_address'], path])]
def get_share_path_by_export_location(self, server, export_location):
return export_location.split(':')[-1]
@nfs_synchronized
def disable_access_for_maintenance(self, server, share_name):
maintenance_file = self._get_maintenance_file_path(share_name)
backup_exports = [
'cat', const.NFS_EXPORTS_FILE,
'| grep', share_name,
'| sudo tee', maintenance_file
]
self._ssh_exec(server, backup_exports)
local_path = os.path.join(self.configuration.share_mount_path,
share_name)
self._ssh_exec(server, ['sudo', 'exportfs', '-u', local_path])
self._sync_nfs_temp_and_perm_files(server)
@nfs_synchronized
def restore_access_after_maintenance(self, server, share_name):
maintenance_file = self._get_maintenance_file_path(share_name)
restore_exports = [
'cat', maintenance_file,
'| sudo tee -a', const.NFS_EXPORTS_FILE,
'&& sudo exportfs -r',
'&& sudo rm -f', maintenance_file
]
self._ssh_exec(server, restore_exports)
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."""
share_path = os.path.join(self.configuration.share_mount_path,
share_name)
create_cmd = [
'sudo', 'net', 'conf', 'addshare', share_name, share_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['public_address'], 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(_LW("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_level,
access_to):
"""Add access for share."""
if access_type != 'ip':
reason = _('Only ip access type allowed.')
raise exception.InvalidShareAccess(reason=reason)
if access_level != const.ACCESS_LEVEL_RW:
raise exception.InvalidShareAccessLevel(level=access_level)
hosts = self._get_allow_hosts(server, share_name)
if access_to in hosts:
raise exception.ShareAccessExists(
access_type=access_type, access=access_to)
hosts.append(access_to)
self._set_allow_hosts(server, hosts, share_name)
def deny_access(self, server, share_name, access, force=False):
"""Remove access for share."""
access_to, access_level = access['access_to'], access['access_level']
if access_level != const.ACCESS_LEVEL_RW:
return
try:
hosts = self._get_allow_hosts(server, share_name)
if access_to in hosts:
# Access rule can be in error state, if so
# it can be absent in rules, hence - skip removal.
hosts.remove(access_to)
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])
@staticmethod
def _get_share_group_name_from_export_location(export_location):
if '/' in export_location and '\\' in export_location:
pass
elif export_location.startswith('\\\\'):
return export_location.split('\\')[-1]
elif export_location.startswith('//'):
return export_location.split('/')[-1]
msg = _("Got incorrect CIFS export location '%s'.") % export_location
raise exception.InvalidShare(reason=msg)
def get_exports_for_share(self, server, old_export_location):
self._verify_server_has_public_address(server)
group_name = self._get_share_group_name_from_export_location(
old_export_location)
data = dict(ip=server['public_address'], share=group_name)
return ['\\\\%(ip)s\\%(share)s' % data]
def get_share_path_by_export_location(self, server, export_location):
# Get name of group that contains share data on CIFS server
group_name = self._get_share_group_name_from_export_location(
export_location)
# Get parameter 'path' from group that belongs to current share
(out, __) = self._ssh_exec(
server, ['sudo', 'net', 'conf', 'getparm', group_name, 'path'])
# Remove special symbols from response and return path
return out.strip()
def disable_access_for_maintenance(self, server, share_name):
maintenance_file = self._get_maintenance_file_path(share_name)
allowed_hosts = " ".join(self._get_allow_hosts(server, share_name))
backup_exports = [
'echo', "'%s'" % allowed_hosts, '| sudo tee', maintenance_file
]
self._ssh_exec(server, backup_exports)
self._set_allow_hosts(server, [], share_name)
def restore_access_after_maintenance(self, server, share_name):
maintenance_file = self._get_maintenance_file_path(share_name)
(exports, __) = self._ssh_exec(server, ['cat', maintenance_file])
self._set_allow_hosts(server, exports.split(), share_name)
self._ssh_exec(server, ['sudo rm -f', maintenance_file])

View File

@ -0,0 +1,444 @@
# Copyright 2015 Mirantis 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 os
import re
from oslo_log import log
from oslo_utils import excutils
from manila.common import constants as const
from manila import exception
from manila.i18n import _
from manila.i18n import _LW
from manila import utils
LOG = log.getLogger(__name__)
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 configure_access(self, server, share_name):
"""Configure server before allowing access."""
pass
def allow_access(self, server, share_name, access_type, access_level,
access_to):
"""Allow access to the host."""
raise NotImplementedError()
def deny_access(self, server, share_name, access, force=False):
"""Deny access to the host."""
raise NotImplementedError()
@staticmethod
def _verify_server_has_public_address(server):
if 'public_address' not in server:
raise exception.ManilaException(
_("Can not get 'public_address' for generation of export."))
def get_exports_for_share(self, server, old_export_location):
"""Returns list of exports based on server info."""
raise NotImplementedError()
def get_share_path_by_export_location(self, server, export_location):
"""Returns share path by its export location."""
raise NotImplementedError()
def disable_access_for_maintenance(self, server, share_name):
"""Disables access to share to perform maintenance operations."""
def restore_access_after_maintenance(self, server, share_name):
"""Enables access to share after maintenance operations were done."""
def _get_maintenance_file_path(self, share_name):
return os.path.join(self.configuration.share_mount_path,
"%s.maintenance" % share_name)
def nfs_synchronized(f):
def wrapped_func(self, *args, **kwargs):
key = "nfs-%s" % args[0]["instance_id"]
@utils.synchronized(key)
def source_func(self, *args, **kwargs):
return f(self, *args, **kwargs)
return source_func(self, *args, **kwargs)
return wrapped_func
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['public_address'],
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."""
@nfs_synchronized
def allow_access(self, server, share_name, access_type, access_level,
access_to):
"""Allow access to the host."""
local_path = os.path.join(self.configuration.share_mount_path,
share_name)
if access_type != 'ip':
msg = _('only ip access type allowed')
raise exception.InvalidShareAccess(reason=msg)
# check if presents in export
out, err = self._ssh_exec(server, ['sudo', 'exportfs'])
out = re.search(
re.escape(local_path) + '[\s\n]*' + re.escape(access_to), out)
if out is not None:
raise exception.ShareAccessExists(access_type=access_type,
access=access_to)
self._ssh_exec(
server,
['sudo', 'exportfs', '-o', '%s,no_subtree_check' % access_level,
':'.join([access_to, local_path])])
self._sync_nfs_temp_and_perm_files(server)
@nfs_synchronized
def deny_access(self, server, share_name, 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['access_to'], local_path])])
self._sync_nfs_temp_and_perm_files(server)
def _sync_nfs_temp_and_perm_files(self, server):
"""Sync changes of exports with permanent NFS config file.
This is required to ensure, that after share server reboot, exports
still exist.
"""
sync_cmd = [
'sudo', 'cp', const.NFS_EXPORTS_FILE_TEMP, const.NFS_EXPORTS_FILE
]
self._ssh_exec(server, sync_cmd)
self._ssh_exec(server, ['sudo', 'exportfs', '-a'])
def get_exports_for_share(self, server, old_export_location):
self._verify_server_has_public_address(server)
path = old_export_location.split(':')[-1]
return [':'.join([server['public_address'], path])]
def get_share_path_by_export_location(self, server, export_location):
return export_location.split(':')[-1]
@nfs_synchronized
def disable_access_for_maintenance(self, server, share_name):
maintenance_file = self._get_maintenance_file_path(share_name)
backup_exports = [
'cat', const.NFS_EXPORTS_FILE,
'| grep', share_name,
'| sudo tee', maintenance_file
]
self._ssh_exec(server, backup_exports)
local_path = os.path.join(self.configuration.share_mount_path,
share_name)
self._ssh_exec(server, ['sudo', 'exportfs', '-u', local_path])
self._sync_nfs_temp_and_perm_files(server)
@nfs_synchronized
def restore_access_after_maintenance(self, server, share_name):
maintenance_file = self._get_maintenance_file_path(share_name)
restore_exports = [
'cat', maintenance_file,
'| sudo tee -a', const.NFS_EXPORTS_FILE,
'&& sudo exportfs -r',
'&& sudo rm -f', maintenance_file
]
self._ssh_exec(server, restore_exports)
class CIFSHelperIPAccess(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. This class allows
to define access to shares by IPs with RW access level.
"""
def __init__(self, *args):
super(CIFSHelperIPAccess, self).__init__(*args)
self.export_format = '\\\\%s\\%s'
self.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',
}
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."""
share_path = os.path.join(self.configuration.share_mount_path,
share_name)
create_cmd = [
'sudo', 'net', 'conf', 'addshare', share_name, share_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)
for param, value in self.parameters.items():
self._ssh_exec(server, ['sudo', 'net', 'conf', 'setparm',
share_name, param, value])
return self.export_format % (server['public_address'], 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(_LW("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_level,
access_to):
"""Add access for share."""
if access_type != 'ip':
reason = _('Only ip access type allowed.')
raise exception.InvalidShareAccess(reason=reason)
if access_level != const.ACCESS_LEVEL_RW:
raise exception.InvalidShareAccessLevel(level=access_level)
hosts = self._get_allow_hosts(server, share_name)
if access_to in hosts:
raise exception.ShareAccessExists(
access_type=access_type, access=access_to)
hosts.append(access_to)
self._set_allow_hosts(server, hosts, share_name)
def deny_access(self, server, share_name, access, force=False):
"""Remove access for share."""
access_to, access_level = access['access_to'], access['access_level']
if access_level != const.ACCESS_LEVEL_RW:
return
try:
hosts = self._get_allow_hosts(server, share_name)
if access_to in hosts:
# Access rule can be in error state, if so
# it can be absent in rules, hence - skip removal.
hosts.remove(access_to)
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])
@staticmethod
def _get_share_group_name_from_export_location(export_location):
if '/' in export_location and '\\' in export_location:
pass
elif export_location.startswith('\\\\'):
return export_location.split('\\')[-1]
elif export_location.startswith('//'):
return export_location.split('/')[-1]
msg = _("Got incorrect CIFS export location '%s'.") % export_location
raise exception.InvalidShare(reason=msg)
def get_exports_for_share(self, server, old_export_location):
self._verify_server_has_public_address(server)
group_name = self._get_share_group_name_from_export_location(
old_export_location)
data = dict(ip=server['public_address'], share=group_name)
return ['\\\\%(ip)s\\%(share)s' % data]
def get_share_path_by_export_location(self, server, export_location):
# Get name of group that contains share data on CIFS server
group_name = self._get_share_group_name_from_export_location(
export_location)
# Get parameter 'path' from group that belongs to current share
(out, __) = self._ssh_exec(
server, ['sudo', 'net', 'conf', 'getparm', group_name, 'path'])
# Remove special symbols from response and return path
return out.strip()
def disable_access_for_maintenance(self, server, share_name):
maintenance_file = self._get_maintenance_file_path(share_name)
allowed_hosts = " ".join(self._get_allow_hosts(server, share_name))
backup_exports = [
'echo', "'%s'" % allowed_hosts, '| sudo tee', maintenance_file
]
self._ssh_exec(server, backup_exports)
self._set_allow_hosts(server, [], share_name)
def restore_access_after_maintenance(self, server, share_name):
maintenance_file = self._get_maintenance_file_path(share_name)
(exports, __) = self._ssh_exec(server, ['cat', maintenance_file])
self._set_allow_hosts(server, exports.split(), share_name)
self._ssh_exec(server, ['sudo rm -f', maintenance_file])
class CIFSHelperUserAccess(CIFSHelperIPAccess):
"""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. This class allows
to define access to shares by usernames with either RW or RO access levels.
"""
def __init__(self, *args):
super(CIFSHelperUserAccess, self).__init__(*args)
self.export_format = '//%s/%s'
self.parameters = {
'browseable': 'yes',
'create mask': '0755',
'hosts allow': '0.0.0.0/0',
'read only': 'no',
}
def allow_access(self, server, share_name, access_type, access_level,
access_to):
"""Add to allow hosts additional access rule."""
if access_type != 'user':
reason = _('Only user access type allowed.')
raise exception.InvalidShareAccess(reason=reason)
all_users = self._get_valid_users(server, share_name)
if access_to in all_users:
raise exception.ShareAccessExists(
access_type=access_type, access=access_to)
user_list = self._get_valid_users(server, share_name, access_level)
user_list.append(access_to)
self._set_valid_users(server, user_list, share_name, access_level)
def deny_access(self, server, share_name, access, force=False):
"""Remove from allow hosts permit rule."""
access_to, access_level = access['access_to'], access['access_level']
users = self._get_valid_users(server, share_name, access_level,
force=force)
if access_to in users:
users.remove(access_to)
self._set_valid_users(server, users, share_name, access_level)
def _get_valid_users(self, server, share_name, access_level=None,
force=True):
if not access_level:
all_users_list = []
for param in ['valid users', 'read list']:
out = ""
try:
(out, _) = self._ssh_exec(server, ['sudo', 'net', 'conf',
'getparm', share_name,
param])
out = out.replace("\"", "")
except exception.ProcessExecutionError:
if not force:
raise
all_users_list += out.split()
return all_users_list
param = self._get_conf_param(access_level)
try:
(out, _) = self._ssh_exec(server, ['sudo', 'net', 'conf',
'getparm', share_name, param])
out = out.replace("\"", "")
return out.split()
except exception.ProcessExecutionError:
if not force:
raise
return []
def _get_conf_param(self, access_level):
if access_level == const.ACCESS_LEVEL_RW:
return 'valid users'
if access_level == const.ACCESS_LEVEL_RO:
return 'read list'
def _set_valid_users(self, server, users, share_name, access_level):
value = "\"" + ' '.join(users) + "\""
param = self._get_conf_param(access_level)
self._ssh_exec(server, ['sudo', 'net', 'conf', 'setparm', share_name,
param, value])

332
manila/share/drivers/lvm.py Normal file
View File

@ -0,0 +1,332 @@
# Copyright 2012 NetApp
# Copyright 2016 Mirantis 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.
"""
LVM Driver for shares.
"""
import math
import os
import re
from oslo_config import cfg
from oslo_log import log
from oslo_utils import importutils
import six
from manila import exception
from manila.i18n import _
from manila.i18n import _LE
from manila.i18n import _LI
from manila.i18n import _LW
from manila.share import driver
from manila.share.drivers import generic
LOG = log.getLogger(__name__)
share_opts = [
cfg.StrOpt('lvm_share_export_root',
default='$state_path/mnt',
help='Base folder where exported shares are located.'),
cfg.StrOpt('lvm_share_export_ip',
help='IP to be added to export string.'),
cfg.IntOpt('lvm_share_mirrors',
default=0,
help='If set, create LVMs with multiple mirrors. Note that '
'this requires lvm_mirrors + 2 PVs with available space.'),
cfg.StrOpt('lvm_share_volume_group',
default='lvm-shares',
help='Name for the VG that will contain exported shares.'),
cfg.ListOpt('lvm_share_helpers',
default=[
'CIFS=manila.share.drivers.helpers.CIFSHelperUserAccess',
'NFS=manila.share.drivers.helpers.NFSHelper',
],
help='Specify list of share export helpers.'),
]
CONF = cfg.CONF
CONF.register_opts(share_opts)
CONF.register_opts(generic.share_opts)
class LVMMixin(driver.ExecuteMixin):
def check_for_setup_error(self):
"""Returns an error if prerequisites aren't met."""
out, err = self._execute('sudo', 'vgs', '--noheadings', '-o', 'name')
volume_groups = out.split()
if self.configuration.lvm_share_volume_group not in volume_groups:
msg = (_("share volume group %s doesn't exist")
% self.configuration.lvm_share_volume_group)
raise exception.InvalidParameterValue(err=msg)
if not self.configuration.lvm_share_export_ip:
msg = (_("share_export_ip isn't specified"))
raise exception.InvalidParameterValue(err=msg)
def _allocate_container(self, share):
sizestr = '%sG' % share['size']
cmd = ['lvcreate', '-L', sizestr, '-n', share['name'],
self.configuration.lvm_share_volume_group]
if self.configuration.lvm_share_mirrors:
cmd += ['-m', self.configuration.lvm_share_mirrors, '--nosync']
terras = int(sizestr[:-1]) / 1024.0
if terras >= 1.5:
rsize = int(2 ** math.ceil(math.log(terras) / math.log(2)))
# NOTE(vish): Next power of two for region size. See:
# http://red.ht/U2BPOD
cmd += ['-R', six.text_type(rsize)]
self._try_execute(*cmd, run_as_root=True)
device_name = self._get_local_path(share)
self._execute('mkfs.%s' % self.configuration.share_volume_fstype,
device_name, run_as_root=True)
def _extend_container(self, share, device_name, size):
cmd = ['lvextend', '-L', '%sG' % size, '-n', device_name]
self._try_execute(*cmd, run_as_root=True)
def _deallocate_container(self, share_name):
"""Deletes a logical volume for share."""
try:
self._try_execute('lvremove', '-f', "%s/%s" %
(self.configuration.lvm_share_volume_group,
share_name), run_as_root=True)
except exception.ProcessExecutionError as exc:
if "not found" not in exc.stderr:
LOG.exception(_LE("Error deleting volume"))
raise
LOG.warning(_LW("Volume not found: %s") % exc.stderr)
def create_snapshot(self, context, snapshot, share_server=None):
"""Creates a snapshot."""
orig_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group,
snapshot['share_name'])
self._try_execute(
'lvcreate', '-L', '%sG' % snapshot['share']['size'],
'--name', snapshot['name'],
'--snapshot', orig_lv_name, run_as_root=True)
def delete_snapshot(self, context, snapshot, share_server=None):
"""Deletes a snapshot."""
self._deallocate_container(snapshot['name'])
class LVMShareDriver(LVMMixin, driver.ShareDriver):
"""Executes commands relating to Shares."""
def __init__(self, *args, **kwargs):
"""Do initialization."""
super(LVMShareDriver, self).__init__([False], *args, **kwargs)
self.configuration.append_config_values(share_opts)
self.configuration.append_config_values(generic.share_opts)
self.configuration.share_mount_path = (
self.configuration.lvm_share_export_root)
self._helpers = None
self.backend_name = self.configuration.safe_get(
'share_backend_name') or 'LVM'
# Set of parameters used for compatibility with
# Generic driver's helpers.
self.share_server = {
'public_address': self.configuration.lvm_share_export_ip,
'instance_id': self.backend_name,
}
def _ssh_exec_as_root(self, server, command):
kwargs = {}
if 'sudo' in command:
kwargs['run_as_root'] = True
command.remove('sudo')
return self._execute(*command, **kwargs)
def do_setup(self, context):
"""Any initialization the volume driver does while starting."""
super(LVMShareDriver, self).do_setup(context)
self._setup_helpers()
def _setup_helpers(self):
"""Initializes protocol-specific NAS drivers."""
self._helpers = {}
for helper_str in self.configuration.lvm_share_helpers:
share_proto, _, import_str = helper_str.partition('=')
helper = importutils.import_class(import_str)
# TODO(rushiagr): better way to handle configuration
# instead of just passing to the helper
self._helpers[share_proto.upper()] = helper(
self._execute, self._ssh_exec_as_root, self.configuration)
def _get_local_path(self, share):
# The escape characters are expected by the device mapper.
escaped_group = (
self.configuration.lvm_share_volume_group.replace('-', '--'))
escaped_name = share['name'].replace('-', '--')
return "/dev/mapper/%s-%s" % (escaped_group, escaped_name)
def _update_share_stats(self):
"""Retrieve stats info from share volume group."""
data = {
'share_backend_name': self.backend_name,
'storage_protocol': 'NFS_CIFS',
'reserved_percentage':
self.configuration.reserved_share_percentage,
'consistency_group_support': None,
'snapshot_support': True,
'driver_name': 'LVMShareDriver',
'pools': self.get_share_server_pools()
}
super(LVMShareDriver, self)._update_share_stats(data)
def get_share_server_pools(self, share_server=None):
out, err = self._execute('sudo', 'vgs',
self.configuration.lvm_share_volume_group,
'--rows')
total_size = re.findall("VSize\s[0-9.]+g", out)[0][6:-1]
free_size = re.findall("VFree\s[0-9.]+g", out)[0][6:-1]
return [{
'pool_name': 'lvm-single-pool',
'total_capacity_gb': float(total_size),
'free_capacity_gb': float(free_size),
'reserved_percentage': 0,
}, ]
def create_share(self, context, share, share_server=None):
self._allocate_container(share)
# create file system
device_name = self._get_local_path(share)
location = self._get_helper(share).create_export(self.share_server,
share['name'])
self._mount_device(share, device_name)
return location
def create_share_from_snapshot(self, context, share, snapshot,
share_server=None):
"""Is called to create share from snapshot."""
self._allocate_container(share)
device_name = self._get_local_path(snapshot)
self._copy_volume(device_name, self._get_local_path(share),
share['size'])
location = self._get_helper(share).create_export(self.share_server,
share['name'])
self._mount_device(share, device_name)
return location
def delete_share(self, context, share, share_server=None):
self._remove_export(context, share)
self._delete_share(context, share)
self._deallocate_container(share['name'])
def _remove_export(self, ctx, share):
"""Removes an access rules for a share."""
mount_path = self._get_mount_path(share)
if os.path.exists(mount_path):
# umount, may be busy
try:
self._execute('umount', '-f', mount_path, run_as_root=True)
except exception.ProcessExecutionError as exc:
if 'device is busy' in six.text_type(exc):
raise exception.ShareBusyException(reason=share['name'])
else:
LOG.info(_LI('Unable to umount: %s'), exc)
# remove dir
try:
os.rmdir(mount_path)
except OSError:
LOG.warning(_LI('Unable to delete %s'), mount_path)
def ensure_share(self, ctx, share, share_server=None):
"""Ensure that storage are mounted and exported."""
device_name = self._get_local_path(share)
self._mount_device(share, device_name)
self._get_helper(share).create_export(self.share_server, share['name'],
recreate=True)
def _delete_share(self, ctx, share):
"""Delete a share."""
try:
self._get_helper(share).remove_export(self.share_server,
share['name'])
except exception.ProcessExecutionError:
LOG.warning(_LI("Can't remove share %r"), share['id'])
except exception.InvalidShare as exc:
LOG.warning(exc.message)
def allow_access(self, ctx, share, access, share_server=None):
"""Allow access to the share."""
self._get_helper(share).allow_access(self.share_server, share['name'],
access['access_type'],
access['access_level'],
access['access_to'])
def deny_access(self, ctx, share, access, share_server=None):
"""Deny access to the share."""
self._get_helper(share).deny_access(self.share_server, share['name'],
access)
def _get_helper(self, share):
if share['share_proto'].lower().startswith('nfs'):
return self._helpers['NFS']
elif share['share_proto'].lower().startswith('cifs'):
return self._helpers['CIFS']
else:
raise exception.InvalidShare(reason='Wrong share protocol')
def _mount_device(self, share, device_name):
"""Mount LVM share and ignore if already mounted."""
mount_path = self._get_mount_path(share)
self._execute('mkdir', '-p', mount_path)
try:
self._execute('mount', device_name, mount_path,
run_as_root=True, check_exit_code=True)
self._execute('chmod', '777', mount_path,
run_as_root=True, check_exit_code=True)
except exception.ProcessExecutionError:
out, err = self._execute('mount', '-l', run_as_root=True)
if device_name in out:
LOG.warning(_LW("%s is already mounted"), device_name)
else:
raise
return mount_path
def _unmount_device(self, share):
mount_path = self._get_mount_path(share)
self._execute('umount', mount_path, run_as_root=True)
self._execute('rmdir', mount_path, run_as_root=True)
def _get_mount_path(self, share):
"""Returns path where share is mounted."""
return os.path.join(self.configuration.share_mount_path,
share['name'])
def _copy_volume(self, srcstr, deststr, size_in_g):
# Use O_DIRECT to avoid thrashing the system buffer cache
extra_flags = ['iflag=direct', 'oflag=direct']
# Check whether O_DIRECT is supported
try:
self._execute('dd', 'count=0', 'if=%s' % srcstr, 'of=%s' % deststr,
*extra_flags, run_as_root=True)
except exception.ProcessExecutionError:
extra_flags = []
# Perform the copy
self._execute('dd', 'if=%s' % srcstr, 'of=%s' % deststr,
'count=%d' % (size_in_g * 1024), 'bs=1M',
*extra_flags, run_as_root=True)
def extend_share(self, share, new_size, share_server=None):
device_name = self._get_local_path(share)
self._extend_container(share, device_name, new_size)
self._execute('resize2fs', device_name, run_as_root=True)

View File

@ -20,13 +20,13 @@ from oslo_log import log
from manila.common import constants
from manila import exception
from manila.i18n import _, _LI
from manila.share.drivers import generic
from manila.share.drivers import helpers
from manila.share.drivers.windows import windows_utils
LOG = log.getLogger(__name__)
class WindowsSMBHelper(generic.NASHelperBase):
class WindowsSMBHelper(helpers.NASHelperBase):
_SHARE_ACCESS_RIGHT_MAP = {
constants.ACCESS_LEVEL_RW: "Change",
constants.ACCESS_LEVEL_RO: "Read"}

View File

@ -34,6 +34,7 @@ def set_defaults(conf):
_POLICY_PATH = os.path.abspath(os.path.join(CONF.state_path,
'manila/tests/policy.json'))
opts.set_defaults(conf, policy_file=_POLICY_PATH)
_safe_set_of_opts(conf, 'share_export_ip', '0.0.0.0')
_safe_set_of_opts(conf, 'service_instance_user', 'fake_user')
_API_PASTE_PATH = os.path.abspath(os.path.join(CONF.state_path,
'etc/manila/api-paste.ini'))

View File

@ -36,7 +36,6 @@ from manila import test
from manila.tests import fake_compute
from manila.tests import fake_service_instance
from manila.tests import fake_share
from manila.tests import fake_utils
from manila.tests import fake_volume
from manila import utils
from manila import volume
@ -2057,484 +2056,3 @@ class GenericDriverEnsureServerTestCase(test.TestCase):
def test_share_servers_are_handled_server_not_provided(self):
self.assertRaises(
exception.ManilaException, fake, self.dhss_true, self._context)
@ddt.ddt
class NFSHelperTestCase(test.TestCase):
"""Test case for NFS helper of generic driver."""
def setUp(self):
super(NFSHelperTestCase, self).setUp()
fake_utils.stub_out_utils_execute(self)
self.fake_conf = manila.share.configuration.Configuration(None)
self._ssh_exec = mock.Mock(return_value=('', ''))
self._execute = mock.Mock(return_value=('', ''))
self._helper = generic.NFSHelper(self._execute, self._ssh_exec,
self.fake_conf)
ip = '10.254.0.3'
self.server = fake_compute.FakeServer(
ip=ip, public_address=ip, instance_id='fake_instance_id')
self.share_name = 'fake_share_name'
def test_create_export(self):
ret = self._helper.create_export(self.server, self.share_name)
expected_location = ':'.join([self.server['public_address'],
os.path.join(CONF.share_mount_path,
self.share_name)])
self.assertEqual(expected_location, ret)
@ddt.data(const.ACCESS_LEVEL_RW, const.ACCESS_LEVEL_RO)
def test_allow_access(self, data):
self.mock_object(self._helper, '_sync_nfs_temp_and_perm_files')
self._helper.allow_access(
self.server, self.share_name, 'ip', data, '10.0.0.2')
local_path = os.path.join(CONF.share_mount_path, self.share_name)
self._ssh_exec.assert_has_calls([
mock.call(self.server, ['sudo', 'exportfs']),
mock.call(self.server, ['sudo', 'exportfs', '-o',
'%s,no_subtree_check' % data,
':'.join(['10.0.0.2', local_path])])
])
self._helper._sync_nfs_temp_and_perm_files.assert_called_once_with(
self.server)
def test_allow_access_no_ip(self):
self.assertRaises(
exception.InvalidShareAccess,
self._helper.allow_access,
self.server, self.share_name,
'fake_type', 'fake_level', 'fake_rule')
@ddt.data(const.ACCESS_LEVEL_RW, const.ACCESS_LEVEL_RO)
def test_deny_access(self, data):
self.mock_object(self._helper, '_sync_nfs_temp_and_perm_files')
local_path = os.path.join(CONF.share_mount_path, self.share_name)
access = dict(
access_to='10.0.0.2', access_type='ip', access_level=data)
self._helper.deny_access(self.server, self.share_name, access)
export_string = ':'.join(['10.0.0.2', local_path])
expected_exec = ['sudo', 'exportfs', '-u', export_string]
self._ssh_exec.assert_called_once_with(self.server, expected_exec)
self._helper._sync_nfs_temp_and_perm_files.assert_called_once_with(
self.server)
def test_sync_nfs_temp_and_perm_files(self):
self._helper._sync_nfs_temp_and_perm_files(self.server)
self._helper._ssh_exec.assert_called_once_with(self.server, mock.ANY)
@ddt.data('/foo/bar', '5.6.7.8:/bar/quuz', '5.6.7.88:/foo/quuz')
def test_get_exports_for_share(self, export_location):
server = dict(public_address='1.2.3.4')
result = self._helper.get_exports_for_share(server, export_location)
path = export_location.split(':')[-1]
self.assertEqual([':'.join([server['public_address'], path])], result)
@ddt.data(
{'public_address_with_suffix': 'foo'},
{'with_prefix_public_address': 'bar'},
{'with_prefix_public_address_and_with_suffix': 'quuz'}, {})
def test_get_exports_for_share_with_error(self, server):
export_location = '1.2.3.4:/foo/bar'
self.assertRaises(
exception.ManilaException,
self._helper.get_exports_for_share, server, export_location)
@ddt.data('/foo/bar', '5.6.7.8:/foo/bar', '5.6.7.88:fake:/foo/bar')
def test_get_share_path_by_export_location(self, export_location):
result = self._helper.get_share_path_by_export_location(
dict(), export_location)
self.assertEqual('/foo/bar', result)
def test_disable_access_for_maintenance(self):
fake_maintenance_path = "fake.path"
share_mount_path = os.path.join(
self._helper.configuration.share_mount_path, self.share_name)
self.mock_object(self._helper, '_ssh_exec')
self.mock_object(self._helper, '_sync_nfs_temp_and_perm_files')
self.mock_object(self._helper, '_get_maintenance_file_path',
mock.Mock(return_value=fake_maintenance_path))
self._helper.disable_access_for_maintenance(
self.server, self.share_name)
self._helper._ssh_exec.assert_any_call(
self.server,
['cat', const.NFS_EXPORTS_FILE,
'| grep', self.share_name,
'| sudo tee', fake_maintenance_path]
)
self._helper._ssh_exec.assert_any_call(
self.server,
['sudo', 'exportfs', '-u', share_mount_path]
)
self._helper._sync_nfs_temp_and_perm_files.assert_called_once_with(
self.server
)
def test_restore_access_after_maintenance(self):
fake_maintenance_path = "fake.path"
self.mock_object(self._helper, '_get_maintenance_file_path',
mock.Mock(return_value=fake_maintenance_path))
self.mock_object(self._helper, '_ssh_exec')
self._helper.restore_access_after_maintenance(
self.server, self.share_name)
self._helper._ssh_exec.assert_called_once_with(
self.server,
['cat', fake_maintenance_path,
'| sudo tee -a', const.NFS_EXPORTS_FILE,
'&& sudo exportfs -r', '&& sudo rm -f',
fake_maintenance_path]
)
@ddt.ddt
class CIFSHelperTestCase(test.TestCase):
"""Test case for CIFS helper of generic driver."""
def setUp(self):
super(CIFSHelperTestCase, self).setUp()
self.server_details = {'instance_id': 'fake',
'public_address': '1.2.3.4', }
self.share_name = 'fake_share_name'
self.fake_conf = manila.share.configuration.Configuration(None)
self._ssh_exec = mock.Mock(return_value=('', ''))
self._execute = mock.Mock(return_value=('', ''))
self._helper = generic.CIFSHelper(self._execute, self._ssh_exec,
self.fake_conf)
self.access = dict(
access_level=const.ACCESS_LEVEL_RW,
access_type='ip',
access_to='1.1.1.1')
def test_init_helper(self):
self._helper.init_helper(self.server_details)
self._helper._ssh_exec.assert_called_once_with(
self.server_details,
['sudo', 'net', 'conf', 'list'],
)
def test_create_export_share_does_not_exist(self):
def fake_ssh_exec(*args, **kwargs):
if 'showshare' in args[1]:
raise exception.ProcessExecutionError()
else:
return ('', '')
self.mock_object(self._helper, '_ssh_exec',
mock.Mock(side_effect=fake_ssh_exec))
ret = self._helper.create_export(self.server_details, self.share_name)
expected_location = '\\\\%s\\%s' % (
self.server_details['public_address'], self.share_name)
self.assertEqual(expected_location, ret)
share_path = os.path.join(
self._helper.configuration.share_mount_path,
self.share_name)
self._helper._ssh_exec.assert_has_calls([
mock.call(
self.server_details,
['sudo', 'net', 'conf', 'showshare', self.share_name, ]
),
mock.call(
self.server_details,
[
'sudo', 'net', 'conf', 'addshare', self.share_name,
share_path, 'writeable=y', 'guest_ok=y',
]
),
mock.call(self.server_details, mock.ANY),
])
def test_create_export_share_exist_recreate_true(self):
ret = self._helper.create_export(self.server_details, self.share_name,
recreate=True)
expected_location = '\\\\%s\\%s' % (
self.server_details['public_address'], self.share_name)
self.assertEqual(expected_location, ret)
share_path = os.path.join(
self._helper.configuration.share_mount_path,
self.share_name)
self._helper._ssh_exec.assert_has_calls([
mock.call(
self.server_details,
['sudo', 'net', 'conf', 'showshare', self.share_name, ]
),
mock.call(
self.server_details,
['sudo', 'net', 'conf', 'delshare', self.share_name, ]
),
mock.call(
self.server_details,
[
'sudo', 'net', 'conf', 'addshare', self.share_name,
share_path, 'writeable=y', 'guest_ok=y',
]
),
mock.call(self.server_details, mock.ANY),
])
def test_create_export_share_exist_recreate_false(self):
self.assertRaises(
exception.ShareBackendException,
self._helper.create_export,
self.server_details,
self.share_name,
recreate=False,
)
self._helper._ssh_exec.assert_has_calls([
mock.call(
self.server_details,
['sudo', 'net', 'conf', 'showshare', self.share_name, ]
),
])
def test_remove_export(self):
self._helper.remove_export(self.server_details, self.share_name)
self._helper._ssh_exec.assert_called_once_with(
self.server_details,
['sudo', 'net', 'conf', 'delshare', self.share_name],
)
def test_remove_export_forcibly(self):
delshare_command = ['sudo', 'net', 'conf', 'delshare', self.share_name]
def fake_ssh_exec(*args, **kwargs):
if delshare_command == args[1]:
raise exception.ProcessExecutionError()
else:
return ('', '')
self.mock_object(self._helper, '_ssh_exec',
mock.Mock(side_effect=fake_ssh_exec))
self._helper.remove_export(self.server_details, self.share_name)
self._helper._ssh_exec.assert_has_calls([
mock.call(
self.server_details,
['sudo', 'net', 'conf', 'delshare', self.share_name],
),
mock.call(
self.server_details,
['sudo', 'smbcontrol', 'all', 'close-share', self.share_name],
),
])
def test_allow_access_ip_exist(self):
hosts = [self.access['access_to'], ]
self.mock_object(self._helper, '_get_allow_hosts',
mock.Mock(return_value=hosts))
self.mock_object(self._helper, '_set_allow_hosts')
self.assertRaises(
exception.ShareAccessExists,
self._helper.allow_access,
self.server_details,
self.share_name,
self.access['access_type'],
self.access['access_level'],
self.access['access_to'])
self._helper._get_allow_hosts.assert_called_once_with(
self.server_details, self.share_name)
self._helper._set_allow_hosts.assert_has_calls([])
def test_allow_access_ip_does_not_exist(self):
hosts = []
self.mock_object(self._helper, '_get_allow_hosts',
mock.Mock(return_value=hosts))
self.mock_object(self._helper, '_set_allow_hosts')
self._helper.allow_access(
self.server_details, self.share_name,
self.access['access_type'], self.access['access_level'],
self.access['access_to'])
self._helper._get_allow_hosts.assert_called_once_with(
self.server_details, self.share_name)
self._helper._set_allow_hosts.assert_called_once_with(
self.server_details, hosts, self.share_name)
def test_allow_access_wrong_type(self):
self.assertRaises(
exception.InvalidShareAccess,
self._helper.allow_access,
self.server_details,
self.share_name, 'fake', const.ACCESS_LEVEL_RW, '1.1.1.1')
@ddt.data(const.ACCESS_LEVEL_RO, 'fake')
def test_allow_access_wrong_access_level(self, data):
self.assertRaises(
exception.InvalidShareAccessLevel,
self._helper.allow_access,
self.server_details,
self.share_name, 'ip', data, '1.1.1.1')
@ddt.data(const.ACCESS_LEVEL_RO, 'fake')
def test_deny_access_unsupported_access_level(self, data):
access = dict(access_to='1.1.1.1', access_level=data)
self.mock_object(self._helper, '_get_allow_hosts')
self.mock_object(self._helper, '_set_allow_hosts')
self._helper.deny_access(self.server_details, self.share_name, access)
self.assertFalse(self._helper._get_allow_hosts.called)
self.assertFalse(self._helper._set_allow_hosts.called)
def test_deny_access_list_has_value(self):
hosts = [self.access['access_to'], ]
self.mock_object(self._helper, '_get_allow_hosts',
mock.Mock(return_value=hosts))
self.mock_object(self._helper, '_set_allow_hosts')
self._helper.deny_access(
self.server_details, self.share_name, self.access)
self._helper._get_allow_hosts.assert_called_once_with(
self.server_details, self.share_name)
self._helper._set_allow_hosts.assert_called_once_with(
self.server_details, [], self.share_name)
def test_deny_access_list_does_not_have_value(self):
hosts = []
self.mock_object(self._helper, '_get_allow_hosts',
mock.Mock(return_value=hosts))
self.mock_object(self._helper, '_set_allow_hosts')
self._helper.deny_access(
self.server_details, self.share_name, self.access)
self._helper._get_allow_hosts.assert_called_once_with(
self.server_details, self.share_name)
self._helper._set_allow_hosts.assert_has_calls([])
def test_deny_access_force(self):
self.mock_object(
self._helper,
'_get_allow_hosts',
mock.Mock(side_effect=exception.ProcessExecutionError()),
)
self.mock_object(self._helper, '_set_allow_hosts')
self._helper.deny_access(
self.server_details, self.share_name, self.access, force=True)
self._helper._get_allow_hosts.assert_called_once_with(
self.server_details, self.share_name)
self._helper._set_allow_hosts.assert_has_calls([])
def test_deny_access_not_force(self):
def raise_process_execution_error(*args, **kwargs):
raise exception.ProcessExecutionError()
self.mock_object(self._helper, '_get_allow_hosts',
mock.Mock(side_effect=raise_process_execution_error))
self.mock_object(self._helper, '_set_allow_hosts')
self.assertRaises(
exception.ProcessExecutionError,
self._helper.deny_access,
self.server_details, self.share_name, self.access)
self._helper._get_allow_hosts.assert_called_once_with(
self.server_details, self.share_name)
self._helper._set_allow_hosts.assert_has_calls([])
@ddt.data(
'', '1.2.3.4:/nfs/like/export', '/1.2.3.4/foo', '\\1.2.3.4\\foo',
'//1.2.3.4\\mixed_slashes_and_backslashes_one',
'\\\\1.2.3.4/mixed_slashes_and_backslashes_two')
def test__get_share_group_name_from_export_location(self, export_location):
self.assertRaises(
exception.InvalidShare,
self._helper._get_share_group_name_from_export_location,
export_location)
@ddt.data('//5.6.7.8/foo', '\\\\5.6.7.8\\foo')
def test_get_exports_for_share(self, export_location):
server = dict(public_address='1.2.3.4')
self.mock_object(
self._helper, '_get_share_group_name_from_export_location',
mock.Mock(side_effect=(
self._helper._get_share_group_name_from_export_location)))
result = self._helper.get_exports_for_share(server, export_location)
expected_export_location = ['\\\\%s\\foo' % server['public_address']]
self.assertEqual(expected_export_location, result)
self._helper._get_share_group_name_from_export_location.\
assert_called_once_with(export_location)
@ddt.data(
{'public_address_with_suffix': 'foo'},
{'with_prefix_public_address': 'bar'},
{'with_prefix_public_address_and_with_suffix': 'quuz'}, {})
def test_get_exports_for_share_with_exception(self, server):
export_location = '1.2.3.4:/foo/bar'
self.assertRaises(
exception.ManilaException,
self._helper.get_exports_for_share, server, export_location)
@ddt.data('//5.6.7.8/foo', '\\\\5.6.7.8\\foo')
def test_get_share_path_by_export_location(self, export_location):
fake_path = ' /bar/quuz\n '
fake_server = dict()
self.mock_object(
self._helper, '_ssh_exec',
mock.Mock(return_value=(fake_path, 'fake')))
self.mock_object(
self._helper, '_get_share_group_name_from_export_location',
mock.Mock(side_effect=(
self._helper._get_share_group_name_from_export_location)))
result = self._helper.get_share_path_by_export_location(
fake_server, export_location)
self.assertEqual('/bar/quuz', result)
self._helper._ssh_exec.assert_called_once_with(
fake_server, ['sudo', 'net', 'conf', 'getparm', 'foo', 'path'])
self._helper._get_share_group_name_from_export_location.\
assert_called_once_with(export_location)
def test_disable_access_for_maintenance(self):
allowed_hosts = ['test', 'test2']
maintenance_path = os.path.join(
self._helper.configuration.share_mount_path,
"%s.maintenance" % self.share_name)
self.mock_object(self._helper, '_set_allow_hosts')
self.mock_object(self._helper, '_get_allow_hosts',
mock.Mock(return_value=allowed_hosts))
self._helper.disable_access_for_maintenance(
self.server_details, self.share_name)
self._helper._get_allow_hosts.assert_called_once_with(
self.server_details, self.share_name)
self._helper._set_allow_hosts.assert_called_once_with(
self.server_details, [], self.share_name)
valid_cmd = ['echo', "'test test2'", '| sudo tee', maintenance_path]
self._helper._ssh_exec.assert_called_once_with(
self.server_details, valid_cmd)
def test_restore_access_after_maintenance(self):
fake_maintenance_path = "test.path"
self.mock_object(self._helper, '_set_allow_hosts')
self.mock_object(self._helper, '_get_maintenance_file_path',
mock.Mock(return_value=fake_maintenance_path))
self.mock_object(self._helper, '_ssh_exec',
mock.Mock(side_effect=[("fake fake2", 0), "fake"]))
self._helper.restore_access_after_maintenance(
self.server_details, self.share_name)
self._helper._set_allow_hosts.assert_called_once_with(
self.server_details, ['fake', 'fake2'], self.share_name)
self._helper._ssh_exec.assert_any_call(
self.server_details, ['cat', fake_maintenance_path])
self._helper._ssh_exec.assert_any_call(
self.server_details, ['sudo rm -f', fake_maintenance_path])

View File

@ -0,0 +1,765 @@
# Copyright 2015 Mirantis 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 os
import ddt
import mock
from oslo_config import cfg
from manila.common import constants as const
from manila import exception
import manila.share.configuration
from manila.share.drivers import helpers
from manila import test
from manila.tests import fake_compute
from manila.tests import fake_utils
CONF = cfg.CONF
@ddt.ddt
class NFSHelperTestCase(test.TestCase):
"""Test case for NFS helper."""
def setUp(self):
super(NFSHelperTestCase, self).setUp()
fake_utils.stub_out_utils_execute(self)
self.fake_conf = manila.share.configuration.Configuration(None)
self._ssh_exec = mock.Mock(return_value=('', ''))
self._execute = mock.Mock(return_value=('', ''))
self._helper = helpers.NFSHelper(self._execute, self._ssh_exec,
self.fake_conf)
ip = '10.254.0.3'
self.server = fake_compute.FakeServer(
ip=ip, public_address=ip, instance_id='fake_instance_id')
self.share_name = 'fake_share_name'
def test_create_export(self):
ret = self._helper.create_export(self.server, self.share_name)
expected_location = ':'.join([self.server['public_address'],
os.path.join(CONF.share_mount_path,
self.share_name)])
self.assertEqual(expected_location, ret)
@ddt.data(const.ACCESS_LEVEL_RW, const.ACCESS_LEVEL_RO)
def test_allow_access(self, data):
self.mock_object(self._helper, '_sync_nfs_temp_and_perm_files')
self._helper.allow_access(
self.server, self.share_name, 'ip', data, '10.0.0.2')
local_path = os.path.join(CONF.share_mount_path, self.share_name)
self._ssh_exec.assert_has_calls([
mock.call(self.server, ['sudo', 'exportfs']),
mock.call(self.server, ['sudo', 'exportfs', '-o',
'%s,no_subtree_check' % data,
':'.join(['10.0.0.2', local_path])])
])
self._helper._sync_nfs_temp_and_perm_files.assert_called_once_with(
self.server)
def test_allow_access_no_ip(self):
self.assertRaises(
exception.InvalidShareAccess,
self._helper.allow_access,
self.server, self.share_name,
'fake_type', 'fake_level', 'fake_rule')
@ddt.data(const.ACCESS_LEVEL_RW, const.ACCESS_LEVEL_RO)
def test_deny_access(self, data):
self.mock_object(self._helper, '_sync_nfs_temp_and_perm_files')
local_path = os.path.join(CONF.share_mount_path, self.share_name)
access = dict(
access_to='10.0.0.2', access_type='ip', access_level=data)
self._helper.deny_access(self.server, self.share_name, access)
export_string = ':'.join(['10.0.0.2', local_path])
expected_exec = ['sudo', 'exportfs', '-u', export_string]
self._ssh_exec.assert_called_once_with(self.server, expected_exec)
self._helper._sync_nfs_temp_and_perm_files.assert_called_once_with(
self.server)
def test_sync_nfs_temp_and_perm_files(self):
self._helper._sync_nfs_temp_and_perm_files(self.server)
self._helper._ssh_exec.assert_has_calls(
[mock.call(self.server, mock.ANY) for i in range(1)])
@ddt.data('/foo/bar', '5.6.7.8:/bar/quuz', '5.6.7.88:/foo/quuz')
def test_get_exports_for_share(self, export_location):
server = dict(public_address='1.2.3.4')
result = self._helper.get_exports_for_share(server, export_location)
path = export_location.split(':')[-1]
self.assertEqual([':'.join([server['public_address'], path])], result)
@ddt.data(
{'public_address_with_suffix': 'foo'},
{'with_prefix_public_address': 'bar'},
{'with_prefix_public_address_and_with_suffix': 'quuz'}, {})
def test_get_exports_for_share_with_error(self, server):
export_location = '1.2.3.4:/foo/bar'
self.assertRaises(
exception.ManilaException,
self._helper.get_exports_for_share, server, export_location)
@ddt.data('/foo/bar', '5.6.7.8:/foo/bar', '5.6.7.88:fake:/foo/bar')
def test_get_share_path_by_export_location(self, export_location):
result = self._helper.get_share_path_by_export_location(
dict(), export_location)
self.assertEqual('/foo/bar', result)
def test_disable_access_for_maintenance(self):
fake_maintenance_path = "fake.path"
share_mount_path = os.path.join(
self._helper.configuration.share_mount_path, self.share_name)
self.mock_object(self._helper, '_ssh_exec')
self.mock_object(self._helper, '_sync_nfs_temp_and_perm_files')
self.mock_object(self._helper, '_get_maintenance_file_path',
mock.Mock(return_value=fake_maintenance_path))
self._helper.disable_access_for_maintenance(
self.server, self.share_name)
self._helper._ssh_exec.assert_any_call(
self.server,
['cat', const.NFS_EXPORTS_FILE,
'| grep', self.share_name,
'| sudo tee', fake_maintenance_path]
)
self._helper._ssh_exec.assert_any_call(
self.server,
['sudo', 'exportfs', '-u', share_mount_path]
)
self._helper._sync_nfs_temp_and_perm_files.assert_called_once_with(
self.server
)
def test_restore_access_after_maintenance(self):
fake_maintenance_path = "fake.path"
self.mock_object(self._helper, '_get_maintenance_file_path',
mock.Mock(return_value=fake_maintenance_path))
self.mock_object(self._helper, '_ssh_exec')
self._helper.restore_access_after_maintenance(
self.server, self.share_name)
self._helper._ssh_exec.assert_called_once_with(
self.server,
['cat', fake_maintenance_path,
'| sudo tee -a', const.NFS_EXPORTS_FILE,
'&& sudo exportfs -r', '&& sudo rm -f',
fake_maintenance_path]
)
@ddt.ddt
class CIFSHelperIPAccessTestCase(test.TestCase):
"""Test case for CIFS helper with IP access."""
def setUp(self):
super(CIFSHelperIPAccessTestCase, self).setUp()
self.server_details = {'instance_id': 'fake',
'public_address': '1.2.3.4', }
self.share_name = 'fake_share_name'
self.fake_conf = manila.share.configuration.Configuration(None)
self._ssh_exec = mock.Mock(return_value=('', ''))
self._execute = mock.Mock(return_value=('', ''))
self._helper = helpers.CIFSHelperIPAccess(self._execute,
self._ssh_exec,
self.fake_conf)
self.access = dict(
access_level=const.ACCESS_LEVEL_RW,
access_type='ip',
access_to='1.1.1.1')
def test_init_helper(self):
self._helper.init_helper(self.server_details)
self._helper._ssh_exec.assert_called_once_with(
self.server_details,
['sudo', 'net', 'conf', 'list'],
)
def test_create_export_share_does_not_exist(self):
def fake_ssh_exec(*args, **kwargs):
if 'showshare' in args[1]:
raise exception.ProcessExecutionError()
else:
return ('', '')
self.mock_object(self._helper, '_ssh_exec',
mock.Mock(side_effect=fake_ssh_exec))
ret = self._helper.create_export(self.server_details, self.share_name)
expected_location = '\\\\%s\\%s' % (
self.server_details['public_address'], self.share_name)
self.assertEqual(expected_location, ret)
share_path = os.path.join(
self._helper.configuration.share_mount_path,
self.share_name)
self._helper._ssh_exec.assert_has_calls([
mock.call(
self.server_details,
['sudo', 'net', 'conf', 'showshare', self.share_name, ]
),
mock.call(
self.server_details,
[
'sudo', 'net', 'conf', 'addshare', self.share_name,
share_path, 'writeable=y', 'guest_ok=y',
]
),
])
def test_create_export_share_exist_recreate_true(self):
ret = self._helper.create_export(self.server_details, self.share_name,
recreate=True)
expected_location = '\\\\%s\\%s' % (
self.server_details['public_address'], self.share_name)
self.assertEqual(expected_location, ret)
share_path = os.path.join(
self._helper.configuration.share_mount_path,
self.share_name)
self._helper._ssh_exec.assert_has_calls([
mock.call(
self.server_details,
['sudo', 'net', 'conf', 'showshare', self.share_name, ]
),
mock.call(
self.server_details,
['sudo', 'net', 'conf', 'delshare', self.share_name, ]
),
mock.call(
self.server_details,
[
'sudo', 'net', 'conf', 'addshare', self.share_name,
share_path, 'writeable=y', 'guest_ok=y',
]
),
])
def test_create_export_share_exist_recreate_false(self):
self.assertRaises(
exception.ShareBackendException,
self._helper.create_export,
self.server_details,
self.share_name,
recreate=False,
)
self._helper._ssh_exec.assert_has_calls([
mock.call(
self.server_details,
['sudo', 'net', 'conf', 'showshare', self.share_name, ]
),
])
def test_remove_export(self):
self._helper.remove_export(self.server_details, self.share_name)
self._helper._ssh_exec.assert_called_once_with(
self.server_details,
['sudo', 'net', 'conf', 'delshare', self.share_name],
)
def test_remove_export_forcibly(self):
delshare_command = ['sudo', 'net', 'conf', 'delshare', self.share_name]
def fake_ssh_exec(*args, **kwargs):
if delshare_command == args[1]:
raise exception.ProcessExecutionError()
else:
return ('', '')
self.mock_object(self._helper, '_ssh_exec',
mock.Mock(side_effect=fake_ssh_exec))
self._helper.remove_export(self.server_details, self.share_name)
self._helper._ssh_exec.assert_has_calls([
mock.call(
self.server_details,
['sudo', 'net', 'conf', 'delshare', self.share_name],
),
mock.call(
self.server_details,
['sudo', 'smbcontrol', 'all', 'close-share', self.share_name],
),
])
def test_allow_access_ip_exist(self):
hosts = [self.access['access_to'], ]
self.mock_object(self._helper, '_get_allow_hosts',
mock.Mock(return_value=hosts))
self.mock_object(self._helper, '_set_allow_hosts')
self.assertRaises(
exception.ShareAccessExists,
self._helper.allow_access,
self.server_details,
self.share_name,
self.access['access_type'],
self.access['access_level'],
self.access['access_to'])
self._helper._get_allow_hosts.assert_called_once_with(
self.server_details, self.share_name)
self._helper._set_allow_hosts.assert_has_calls([])
def test_allow_access_ip_does_not_exist(self):
hosts = []
self.mock_object(self._helper, '_get_allow_hosts',
mock.Mock(return_value=hosts))
self.mock_object(self._helper, '_set_allow_hosts')
self._helper.allow_access(
self.server_details, self.share_name,
self.access['access_type'], self.access['access_level'],
self.access['access_to'])
self._helper._get_allow_hosts.assert_called_once_with(
self.server_details, self.share_name)
self._helper._set_allow_hosts.assert_called_once_with(
self.server_details, hosts, self.share_name)
def test_allow_access_wrong_type(self):
self.assertRaises(
exception.InvalidShareAccess,
self._helper.allow_access,
self.server_details,
self.share_name, 'fake', const.ACCESS_LEVEL_RW, '1.1.1.1')
@ddt.data(const.ACCESS_LEVEL_RO, 'fake')
def test_allow_access_wrong_access_level(self, data):
self.assertRaises(
exception.InvalidShareAccessLevel,
self._helper.allow_access,
self.server_details,
self.share_name, 'ip', data, '1.1.1.1')
@ddt.data(const.ACCESS_LEVEL_RO, 'fake')
def test_deny_access_unsupported_access_level(self, data):
access = dict(access_to='1.1.1.1', access_level=data)
self.mock_object(self._helper, '_get_allow_hosts')
self.mock_object(self._helper, '_set_allow_hosts')
self._helper.deny_access(self.server_details, self.share_name, access)
self.assertFalse(self._helper._get_allow_hosts.called)
self.assertFalse(self._helper._set_allow_hosts.called)
def test_deny_access_list_has_value(self):
hosts = [self.access['access_to'], ]
self.mock_object(self._helper, '_get_allow_hosts',
mock.Mock(return_value=hosts))
self.mock_object(self._helper, '_set_allow_hosts')
self._helper.deny_access(
self.server_details, self.share_name, self.access)
self._helper._get_allow_hosts.assert_called_once_with(
self.server_details, self.share_name)
self._helper._set_allow_hosts.assert_called_once_with(
self.server_details, [], self.share_name)
def test_deny_access_list_does_not_have_value(self):
hosts = []
self.mock_object(self._helper, '_get_allow_hosts',
mock.Mock(return_value=hosts))
self.mock_object(self._helper, '_set_allow_hosts')
self._helper.deny_access(
self.server_details, self.share_name, self.access)
self._helper._get_allow_hosts.assert_called_once_with(
self.server_details, self.share_name)
self._helper._set_allow_hosts.assert_has_calls([])
def test_deny_access_force(self):
self.mock_object(
self._helper,
'_get_allow_hosts',
mock.Mock(side_effect=exception.ProcessExecutionError()),
)
self.mock_object(self._helper, '_set_allow_hosts')
self._helper.deny_access(
self.server_details, self.share_name, self.access, force=True)
self._helper._get_allow_hosts.assert_called_once_with(
self.server_details, self.share_name)
self._helper._set_allow_hosts.assert_has_calls([])
def test_deny_access_not_force(self):
def raise_process_execution_error(*args, **kwargs):
raise exception.ProcessExecutionError()
self.mock_object(self._helper, '_get_allow_hosts',
mock.Mock(side_effect=raise_process_execution_error))
self.mock_object(self._helper, '_set_allow_hosts')
self.assertRaises(
exception.ProcessExecutionError,
self._helper.deny_access,
self.server_details, self.share_name, self.access)
self._helper._get_allow_hosts.assert_called_once_with(
self.server_details, self.share_name)
self._helper._set_allow_hosts.assert_has_calls([])
@ddt.data(
'', '1.2.3.4:/nfs/like/export', '/1.2.3.4/foo', '\\1.2.3.4\\foo',
'//1.2.3.4\\mixed_slashes_and_backslashes_one',
'\\\\1.2.3.4/mixed_slashes_and_backslashes_two')
def test__get_share_group_name_from_export_location(self, export_location):
self.assertRaises(
exception.InvalidShare,
self._helper._get_share_group_name_from_export_location,
export_location)
@ddt.data('//5.6.7.8/foo', '\\\\5.6.7.8\\foo')
def test_get_exports_for_share(self, export_location):
server = dict(public_address='1.2.3.4')
self.mock_object(
self._helper, '_get_share_group_name_from_export_location',
mock.Mock(side_effect=(
self._helper._get_share_group_name_from_export_location)))
result = self._helper.get_exports_for_share(server, export_location)
expected_export_location = ['\\\\%s\\foo' % server['public_address']]
self.assertEqual(expected_export_location, result)
self._helper._get_share_group_name_from_export_location.\
assert_called_once_with(export_location)
@ddt.data(
{'public_address_with_suffix': 'foo'},
{'with_prefix_public_address': 'bar'},
{'with_prefix_public_address_and_with_suffix': 'quuz'}, {})
def test_get_exports_for_share_with_exception(self, server):
export_location = '1.2.3.4:/foo/bar'
self.assertRaises(
exception.ManilaException,
self._helper.get_exports_for_share, server, export_location)
@ddt.data('//5.6.7.8/foo', '\\\\5.6.7.8\\foo')
def test_get_share_path_by_export_location(self, export_location):
fake_path = ' /bar/quuz\n '
fake_server = dict()
self.mock_object(
self._helper, '_ssh_exec',
mock.Mock(return_value=(fake_path, 'fake')))
self.mock_object(
self._helper, '_get_share_group_name_from_export_location',
mock.Mock(side_effect=(
self._helper._get_share_group_name_from_export_location)))
result = self._helper.get_share_path_by_export_location(
fake_server, export_location)
self.assertEqual('/bar/quuz', result)
self._helper._ssh_exec.assert_called_once_with(
fake_server, ['sudo', 'net', 'conf', 'getparm', 'foo', 'path'])
self._helper._get_share_group_name_from_export_location.\
assert_called_once_with(export_location)
def test_disable_access_for_maintenance(self):
allowed_hosts = ['test', 'test2']
maintenance_path = os.path.join(
self._helper.configuration.share_mount_path,
"%s.maintenance" % self.share_name)
self.mock_object(self._helper, '_set_allow_hosts')
self.mock_object(self._helper, '_get_allow_hosts',
mock.Mock(return_value=allowed_hosts))
self._helper.disable_access_for_maintenance(
self.server_details, self.share_name)
self._helper._get_allow_hosts.assert_called_once_with(
self.server_details, self.share_name)
self._helper._set_allow_hosts.assert_called_once_with(
self.server_details, [], self.share_name)
valid_cmd = ['echo', "'test test2'", '| sudo tee', maintenance_path]
self._helper._ssh_exec.assert_called_once_with(
self.server_details, valid_cmd)
def test_restore_access_after_maintenance(self):
fake_maintenance_path = "test.path"
self.mock_object(self._helper, '_set_allow_hosts')
self.mock_object(self._helper, '_get_maintenance_file_path',
mock.Mock(return_value=fake_maintenance_path))
self.mock_object(self._helper, '_ssh_exec',
mock.Mock(side_effect=[("fake fake2", 0), "fake"]))
self._helper.restore_access_after_maintenance(
self.server_details, self.share_name)
self._helper._set_allow_hosts.assert_called_once_with(
self.server_details, ['fake', 'fake2'], self.share_name)
self._helper._ssh_exec.assert_any_call(
self.server_details, ['cat', fake_maintenance_path])
self._helper._ssh_exec.assert_any_call(
self.server_details, ['sudo rm -f', fake_maintenance_path])
@ddt.ddt
class CIFSHelperUserAccessTestCase(test.TestCase):
"""Test case for CIFS helper with user access."""
access_rw = dict(
access_level=const.ACCESS_LEVEL_RW,
access_type='user',
access_to='manila-user')
access_ro = dict(
access_level=const.ACCESS_LEVEL_RO,
access_type='user',
access_to='manila-user')
def setUp(self):
super(CIFSHelperUserAccessTestCase, self).setUp()
self.server_details = {'instance_id': 'fake',
'public_address': '1.2.3.4', }
self.share_name = 'fake_share_name'
self.fake_conf = manila.share.configuration.Configuration(None)
self._ssh_exec = mock.Mock(return_value=('', ''))
self._execute = mock.Mock(return_value=('', ''))
self._helper = helpers.CIFSHelperUserAccess(
self._execute, self._ssh_exec, self.fake_conf)
@ddt.data('ip', 'cert', 'fake')
def test_allow_access_wrong_type(self, wrong_access_type):
self.assertRaises(
exception.InvalidShareAccess,
self._helper.allow_access,
self.server_details,
self.share_name,
wrong_access_type,
const.ACCESS_LEVEL_RW,
'1.1.1.1')
@ddt.data(access_rw, access_ro)
def test_allow_access_ro_rule_does_not_exist(self, access):
users = ['user1', 'user2']
self.mock_object(self._helper, '_get_valid_users',
mock.Mock(return_value=users))
self.mock_object(self._helper, '_set_valid_users')
self._helper.allow_access(
self.server_details, self.share_name,
access['access_type'], access['access_level'],
access['access_to'])
self.assertEqual(
[mock.call(self.server_details, self.share_name),
mock.call(self.server_details, self.share_name,
access['access_level'])],
self._helper._get_valid_users.call_args_list)
self._helper._set_valid_users.assert_called_once_with(
self.server_details,
users,
self.share_name,
access['access_level'])
@ddt.data(access_rw, access_ro)
def test_allow_access_ro_rule_exists(self, access):
users = ['user1', 'user2', 'manila-user']
self.mock_object(self._helper, '_get_valid_users',
mock.Mock(return_value=users))
self.assertRaises(
exception.ShareAccessExists,
self._helper.allow_access,
self.server_details,
self.share_name,
access['access_type'],
access['access_level'],
access['access_to'])
@ddt.data(access_rw, access_ro)
def test_deny_access_list_has_value(self, access):
users = ['user1', 'user2', 'manila-user']
self.mock_object(self._helper, '_get_valid_users',
mock.Mock(return_value=users))
self.mock_object(self._helper, '_set_valid_users')
self._helper.deny_access(
self.server_details, self.share_name, access)
self._helper._get_valid_users.assert_called_once_with(
self.server_details,
self.share_name,
access['access_level'],
force=False)
self._helper._set_valid_users.assert_called_once_with(
self.server_details, ['user1', 'user2'], self.share_name,
access['access_level'])
@ddt.data(access_rw, access_ro)
def test_deny_access_list_does_not_have_value(self, access):
users = []
self.mock_object(self._helper, '_get_valid_users',
mock.Mock(return_value=users))
self.mock_object(self._helper, '_set_valid_users')
self._helper.deny_access(
self.server_details, self.share_name, access)
self._helper._get_valid_users.assert_called_once_with(
self.server_details,
self.share_name,
access['access_level'],
force=False)
self._helper._set_valid_users.assert_has_calls([])
@ddt.data(access_rw, access_ro)
def test_deny_access_force_access_exists(self, access):
users = ['user1', 'user2', 'manila-user']
self.mock_object(self._helper, '_get_valid_users',
mock.Mock(return_value=users))
self.mock_object(self._helper, '_set_valid_users')
self._helper.deny_access(
self.server_details, self.share_name, access, force=True)
self._helper._get_valid_users.assert_called_once_with(
self.server_details,
self.share_name,
access['access_level'],
force=True)
self._helper._set_valid_users.assert_called_once_with(
self.server_details, ['user1', 'user2'], self.share_name,
access['access_level'])
@ddt.data(access_rw, access_ro)
def test_deny_access_force_access_does_not_exist(self, access):
self.mock_object(
self._helper,
'_get_valid_users',
mock.Mock(return_value=[]),
)
self.mock_object(self._helper, '_set_valid_users')
self._helper.deny_access(
self.server_details, self.share_name, access, force=True)
self._helper._get_valid_users.assert_called_once_with(
self.server_details,
self.share_name,
access['access_level'],
force=True)
self._helper._set_valid_users.assert_has_calls([])
@ddt.data(access_rw, access_ro)
def test_deny_access_force_exc(self, access):
self.mock_object(
self._helper,
'_get_valid_users',
mock.Mock(side_effect=exception.ProcessExecutionError()),
)
self.mock_object(self._helper, '_set_valid_users')
self.assertRaises(exception.ProcessExecutionError,
self._helper.deny_access,
self.server_details,
self.share_name,
access,
force=True)
self._helper._get_valid_users.assert_called_once_with(
self.server_details,
self.share_name,
access['access_level'],
force=True)
def test_get_conf_param_rw(self):
result = self._helper._get_conf_param(const.ACCESS_LEVEL_RW)
self.assertEqual('valid users', result)
def test_get_conf_param_ro(self):
result = self._helper._get_conf_param(const.ACCESS_LEVEL_RO)
self.assertEqual('read list', result)
@ddt.data(False, True)
def test_get_valid_users(self, force):
users = ("\"manila-user\" \"user1\" \"user2\"", None)
self.mock_object(self._helper, '_ssh_exec',
mock.Mock(return_value=users))
result = self._helper._get_valid_users(self.server_details,
self.share_name,
const.ACCESS_LEVEL_RW,
force=force)
self.assertEqual(['manila-user', 'user1', 'user2'], result)
self._helper._ssh_exec.assert_called_once_with(
self.server_details,
['sudo', 'net', 'conf', 'getparm', self.share_name, 'valid users'])
@ddt.data(False, True)
def test_get_valid_users_access_level_none(self, force):
def fake_ssh_exec(*args, **kwargs):
if 'valid users' in args[1]:
return ("\"user1\"", '')
else:
return ("\"user2\"", '')
self.mock_object(self._helper, '_ssh_exec',
mock.Mock(side_effect=fake_ssh_exec))
result = self._helper._get_valid_users(self.server_details,
self.share_name,
force=force)
self.assertEqual(['user1', 'user2'], result)
for param in ['read list', 'valid users']:
self._helper._ssh_exec.assert_any_call(
self.server_details,
['sudo', 'net', 'conf', 'getparm', self.share_name, param])
def test_get_valid_users_access_level_none_with_exc(self):
self.mock_object(
self._helper,
'_ssh_exec',
mock.Mock(side_effect=exception.ProcessExecutionError()))
self.assertRaises(exception.ProcessExecutionError,
self._helper._get_valid_users,
self.server_details,
self.share_name,
force=False)
self._helper._ssh_exec.assert_called_once_with(
self.server_details,
['sudo', 'net', 'conf', 'getparm', self.share_name, 'valid users'])
def test_get_valid_users_force_with_exc(self):
self.mock_object(
self._helper,
'_ssh_exec',
mock.Mock(side_effect=exception.ProcessExecutionError()))
result = self._helper._get_valid_users(self.server_details,
self.share_name,
const.ACCESS_LEVEL_RW)
self.assertEqual([], result)
self._helper._ssh_exec.assert_called_once_with(
self.server_details,
['sudo', 'net', 'conf', 'getparm', self.share_name, 'valid users'])
def test_get_valid_users_not_force_with_exc(self):
self.mock_object(
self._helper,
'_ssh_exec',
mock.Mock(side_effect=exception.ProcessExecutionError()))
self.assertRaises(exception.ProcessExecutionError,
self._helper._get_valid_users, self.server_details,
self.share_name, const.ACCESS_LEVEL_RW, force=False)
self._helper._ssh_exec.assert_called_once_with(
self.server_details,
['sudo', 'net', 'conf', 'getparm', self.share_name, 'valid users'])
def test_set_valid_users(self):
self.mock_object(self._helper, '_ssh_exec', mock.Mock())
self._helper._set_valid_users(self.server_details, ['user1', 'user2'],
self.share_name, const.ACCESS_LEVEL_RW)
self._helper._ssh_exec.assert_called_once_with(
self.server_details,
['sudo', 'net', 'conf', 'setparm', self.share_name,
'valid users', '"user1 user2"'])

View File

@ -0,0 +1,515 @@
# Copyright 2012 NetApp
# 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.
"""Unit tests for the LVM driver module."""
import os
import mock
from oslo_config import cfg
from manila import context
from manila import exception
from manila.share import configuration
from manila.share.drivers import lvm
from manila import test
from manila.tests.db import fakes as db_fakes
from manila.tests import fake_utils
CONF = cfg.CONF
def fake_share(**kwargs):
share = {
'id': 'fakeid',
'name': 'fakename',
'size': 1,
'share_proto': 'NFS',
'export_location': '127.0.0.1:/mnt/nfs/volume-00002',
}
share.update(kwargs)
return db_fakes.FakeModel(share)
def fake_snapshot(**kwargs):
snapshot = {
'id': 'fakesnapshotid',
'share_name': 'fakename',
'share_id': 'fakeid',
'name': 'fakesnapshotname',
'share_proto': 'NFS',
'export_location': '127.0.0.1:/mnt/nfs/volume-00002',
'share': {'size': 1},
}
snapshot.update(kwargs)
return db_fakes.FakeModel(snapshot)
def fake_access(**kwargs):
access = {
'id': 'fakeaccid',
'access_type': 'ip',
'access_to': '10.0.0.2',
'access_level': 'rw',
'state': 'active',
}
access.update(kwargs)
return db_fakes.FakeModel(access)
class LVMShareDriverTestCase(test.TestCase):
"""Tests LVMShareDriver."""
def setUp(self):
super(LVMShareDriverTestCase, self).setUp()
fake_utils.stub_out_utils_execute(self)
self._context = context.get_admin_context()
CONF.set_default('lvm_share_volume_group', 'fakevg')
CONF.set_default('lvm_share_export_ip', '10.0.0.1')
CONF.set_default('driver_handles_share_servers', False)
CONF.set_default('reserved_share_percentage', 50)
self._helper_cifs = mock.Mock()
self._helper_nfs = mock.Mock()
self.fake_conf = configuration.Configuration(None)
self._db = mock.Mock()
self._os = lvm.os = mock.Mock()
self._os.path.join = os.path.join
self._driver = lvm.LVMShareDriver(self._db,
configuration=self.fake_conf)
self._driver._helpers = {
'CIFS': self._helper_cifs,
'NFS': self._helper_nfs,
}
self.share = fake_share()
self.access = fake_access()
self.snapshot = fake_snapshot()
self.server = {
'public_address': self.fake_conf.lvm_share_export_ip,
'instance_id': 'LVM',
}
# Used only to test compatibility with share manager
self.share_server = "fake_share_server"
def tearDown(self):
super(LVMShareDriverTestCase, self).tearDown()
fake_utils.fake_execute_set_repliers([])
fake_utils.fake_execute_clear_log()
def test_do_setup(self):
CONF.set_default('lvm_share_helpers', ['NFS=fakenfs'])
lvm.importutils = mock.Mock()
lvm.importutils.import_class.return_value = self._helper_nfs
self._driver.do_setup(self._context)
lvm.importutils.import_class.assert_has_calls([
mock.call('fakenfs')
])
def test_check_for_setup_error(self):
def exec_runner(*ignore_args, **ignore_kwargs):
return '\n fake1\n fakevg\n fake2\n', ''
expected_exec = [
'sudo vgs --noheadings -o name',
]
fake_utils.fake_execute_set_repliers([(expected_exec[0], exec_runner)])
self._driver.check_for_setup_error()
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
def test_check_for_setup_error_no_vg(self):
def exec_runner(*ignore_args, **ignore_kwargs):
return '\n fake0\n fake1\n fake2\n', ''
fake_utils.fake_execute_set_repliers([('sudo vgs --noheadings -o name',
exec_runner)])
self.assertRaises(exception.InvalidParameterValue,
self._driver.check_for_setup_error)
def test_check_for_setup_error_no_export_ip(self):
def exec_runner(*ignore_args, **ignore_kwargs):
return '\n fake1\n fakevg\n fake2\n', ''
fake_utils.fake_execute_set_repliers([('sudo vgs --noheadings -o name',
exec_runner)])
CONF.set_default('lvm_share_export_ip', None)
self.assertRaises(exception.InvalidParameterValue,
self._driver.check_for_setup_error)
def test_local_path_normal(self):
share = fake_share(name='fake_sharename')
CONF.set_default('lvm_share_volume_group', 'fake_vg')
ret = self._driver._get_local_path(share)
self.assertEqual('/dev/mapper/fake_vg-fake_sharename', ret)
def test_local_path_escapes(self):
share = fake_share(name='fake-sharename')
CONF.set_default('lvm_share_volume_group', 'fake-vg')
ret = self._driver._get_local_path(share)
self.assertEqual('/dev/mapper/fake--vg-fake--sharename', ret)
def test_create_share(self):
self._helper_nfs.create_export.return_value = 'fakelocation'
self._driver._mount_device = mock.Mock()
ret = self._driver.create_share(self._context, self.share,
self.share_server)
CONF.set_default('lvm_share_mirrors', 0)
self._driver._mount_device.assert_called_with(
self.share, '/dev/mapper/fakevg-fakename')
expected_exec = [
'lvcreate -L 1G -n fakename fakevg',
'mkfs.ext4 /dev/mapper/fakevg-fakename',
]
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
self.assertEqual('fakelocation', ret)
def test_create_share_from_snapshot(self):
CONF.set_default('lvm_share_mirrors', 0)
self._driver._mount_device = mock.Mock()
snapshot_instance = {
'snapshot_id': 'fakesnapshotid',
'name': 'fakename'
}
mount_share = '/dev/mapper/fakevg-fakename'
mount_snapshot = '/dev/mapper/fakevg-fakename'
self._helper_nfs.create_export.return_value = 'fakelocation'
self._driver.create_share_from_snapshot(self._context,
self.share,
snapshot_instance,
self.share_server)
self._driver._mount_device.assert_called_with(self.share,
mount_snapshot)
expected_exec = [
'lvcreate -L 1G -n fakename fakevg',
'mkfs.ext4 /dev/mapper/fakevg-fakename',
("dd count=0 if=%s of=%s iflag=direct oflag=direct" %
(mount_snapshot, mount_share)),
("dd if=%s of=%s count=1024 bs=1M iflag=direct oflag=direct" %
(mount_snapshot, mount_share)),
]
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
def test_create_share_mirrors(self):
share = fake_share(size='2048')
CONF.set_default('lvm_share_mirrors', 2)
self._helper_nfs.create_export.return_value = 'fakelocation'
self._driver._mount_device = mock.Mock()
ret = self._driver.create_share(self._context, share,
self.share_server)
self._driver._mount_device.assert_called_with(
share, '/dev/mapper/fakevg-fakename')
expected_exec = [
'lvcreate -L 2048G -n fakename fakevg -m 2 --nosync -R 2',
'mkfs.ext4 /dev/mapper/fakevg-fakename',
]
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
self.assertEqual('fakelocation', ret)
def test_deallocate_container(self):
expected_exec = ['lvremove -f fakevg/fakename']
self._driver._deallocate_container(self.share['name'])
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
def test_deallocate_container_error(self):
def _fake_exec(*args, **kwargs):
raise exception.ProcessExecutionError(stderr="error")
self.mock_object(self._driver, '_try_execute', _fake_exec)
self.assertRaises(exception.ProcessExecutionError,
self._driver._deallocate_container,
self.share['name'])
def test_deallocate_container_not_found_error(self):
def _fake_exec(*args, **kwargs):
raise exception.ProcessExecutionError(stderr="not found")
self.mock_object(self._driver, '_try_execute', _fake_exec)
self._driver._deallocate_container(self.share['name'])
@mock.patch.object(lvm.LVMShareDriver, '_update_share_stats', mock.Mock())
def test_get_share_stats(self):
with mock.patch.object(self._driver, '_stats', mock.Mock) as stats:
self.assertEqual(stats, self._driver.get_share_stats())
self.assertFalse(self._driver._update_share_stats.called)
@mock.patch.object(lvm.LVMShareDriver, '_update_share_stats', mock.Mock())
def test_get_share_stats_refresh(self):
with mock.patch.object(self._driver, '_stats', mock.Mock) as stats:
self.assertEqual(stats,
self._driver.get_share_stats(refresh=True))
self._driver._update_share_stats.assert_called_once_with()
def test_remove_export(self):
mount_path = self._get_mount_path(self.share)
self._os.path.exists.return_value = True
self._driver._remove_export(self._context, self.share)
expected_exec = [
"umount -f %s" % (mount_path,),
]
self._os.path.exists.assert_called_with(mount_path)
self._os.rmdir.assert_called_with(mount_path)
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
def test_remove_export_is_busy_error(self):
def exec_runner(*ignore_args, **ignore_kwargs):
raise exception.ProcessExecutionError(stderr='device is busy')
self._os.path.exists.return_value = True
mount_path = self._get_mount_path(self.share)
expected_exec = [
"umount -f %s" % (mount_path),
]
fake_utils.fake_execute_set_repliers([(expected_exec[0], exec_runner)])
self.assertRaises(exception.ShareBusyException,
self._driver._remove_export, self._context,
self.share)
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
def test_remove_export_error(self):
def exec_runner(*ignore_args, **ignore_kwargs):
raise exception.ProcessExecutionError(stderr='fake error')
mount_path = self._get_mount_path(self.share)
expected_exec = [
"umount -f %s" % (mount_path),
]
fake_utils.fake_execute_set_repliers([(expected_exec[0], exec_runner)])
self._os.path.exists.return_value = True
self._driver._remove_export(self._context, self.share)
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
def test_remove_export_rmdir_error(self):
mount_path = self._get_mount_path(self.share)
self._os.path.exists.return_value = True
self.mock_object(self._os, 'rmdir', mock.Mock(side_effect=OSError))
self._driver._remove_export(self._context, self.share)
expected_exec = [
"umount -f %s" % (mount_path,),
]
self._os.path.exists.assert_called_with(mount_path)
self._os.rmdir.assert_called_with(mount_path)
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
def test_create_snapshot(self):
self._driver.create_snapshot(self._context, self.snapshot,
self.share_server)
expected_exec = [
("lvcreate -L 1G --name fakesnapshotname --snapshot "
"%s/fakename" % (CONF.lvm_share_volume_group,)),
]
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
def test_ensure_share(self):
device_name = '/dev/mapper/fakevg-fakename'
with mock.patch.object(self._driver,
'_mount_device',
mock.Mock(return_value='fake_location')):
self._driver.ensure_share(self._context, self.share,
self.share_server)
self._driver._mount_device.assert_called_with(self.share,
device_name)
self._helper_nfs.create_export.assert_called_once_with(
self.server, self.share['name'], recreate=True)
def test_delete_share(self):
mount_path = self._get_mount_path(self.share)
self._helper_nfs.remove_export(mount_path, self.share['name'])
self._driver._delete_share(self._context, self.share)
def test_delete_snapshot(self):
expected_exec = ['lvremove -f fakevg/fakesnapshotname']
self._driver.delete_snapshot(self._context, self.snapshot,
self.share_server)
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
def test_delete_share_invalid_share(self):
self._driver._get_helper = mock.Mock(
side_effect=exception.InvalidShare(reason='fake'))
self._driver.delete_share(self._context, self.share, self.share_server)
def test_delete_share_process_execution_error(self):
self.mock_object(
self._helper_nfs,
'remove_export',
mock.Mock(side_effect=exception.ProcessExecutionError))
self._driver._delete_share(self._context, self.share)
self._helper_nfs.remove_export.assert_called_once_with(
self.server,
self.share['name'])
def test_allow_access(self):
mount_path = self._get_mount_path(self.share)
self._helper_nfs.allow_access(mount_path,
self.share['name'],
self.access['access_type'],
self.access['access_to'])
self._driver.allow_access(self._context, self.share, self.access,
self.share_server)
def test_deny_access(self):
mount_path = self._get_mount_path(self.share)
self._helper_nfs.deny_access(mount_path,
self.share['name'],
self.access['access_type'],
self.access['access_to'])
self._driver.deny_access(self._context, self.share, self.access,
self.share_server)
def test_mount_device(self):
mount_path = self._get_mount_path(self.share)
ret = self._driver._mount_device(self.share, 'fakedevice')
expected_exec = [
"mkdir -p %s" % (mount_path,),
"mount fakedevice %s" % (mount_path,),
"chmod 777 %s" % (mount_path,),
]
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
self.assertEqual(mount_path, ret)
def test_mount_device_already(self):
def exec_runner(*args, **kwargs):
if 'mount' in args and '-l' not in args:
raise exception.ProcessExecutionError()
else:
return 'fakedevice', ''
self.mock_object(self._driver, '_execute', exec_runner)
mount_path = self._get_mount_path(self.share)
ret = self._driver._mount_device(self.share, 'fakedevice')
self.assertEqual(mount_path, ret)
def test_mount_device_error(self):
def exec_runner(*args, **kwargs):
if 'mount' in args and '-l' not in args:
raise exception.ProcessExecutionError()
else:
return 'fake', ''
self.mock_object(self._driver, '_execute', exec_runner)
self.assertRaises(exception.ProcessExecutionError,
self._driver._mount_device, self.share, 'fakedevice')
def test_get_helper(self):
share_cifs = fake_share(share_proto='CIFS')
share_nfs = fake_share(share_proto='NFS')
share_fake = fake_share(share_proto='FAKE')
self.assertEqual(self._driver._get_helper(share_cifs),
self._helper_cifs)
self.assertEqual(self._driver._get_helper(share_nfs),
self._helper_nfs)
self.assertRaises(exception.InvalidShare, self._driver._get_helper,
share_fake)
def _get_mount_path(self, share):
return os.path.join(CONF.lvm_share_export_root, share['name'])
def test_unmount_device(self):
mount_path = self._get_mount_path(self.share)
self.mock_object(self._driver, '_execute')
self._driver._unmount_device(self.share)
self._driver._execute.assert_any_call('umount', mount_path,
run_as_root=True)
self._driver._execute.assert_any_call('rmdir', mount_path,
run_as_root=True)
def test_extend_share(self):
local_path = self._driver._get_local_path(self.share)
self.mock_object(self._driver, '_extend_container')
self.mock_object(self._driver, '_execute')
self._driver.extend_share(self.share, 3)
self._driver._extend_container.assert_called_once_with(self.share,
local_path, 3)
self._driver._execute.assert_called_once_with('resize2fs', local_path,
run_as_root=True)
def test_ssh_exec_as_root(self):
command = ['fake_command']
self.mock_object(self._driver, '_execute')
self._driver._ssh_exec_as_root('fake_server', command)
self._driver._execute.assert_called_once_with('fake_command')
def test_ssh_exec_as_root_with_sudo(self):
command = ['sudo', 'fake_command']
self.mock_object(self._driver, '_execute')
self._driver._ssh_exec_as_root('fake_server', command)
self._driver._execute.assert_called_once_with('fake_command',
run_as_root=True)
def test_extend_container(self):
self.mock_object(self._driver, '_try_execute')
self._driver._extend_container(self.share, 'device_name', 3)
self._driver._try_execute.assert_called_once_with(
'lvextend',
'-L',
'3G',
'-n',
'device_name',
run_as_root=True)
def test_get_share_server_pools(self):
expected_result = [{
'pool_name': 'lvm-single-pool',
'total_capacity_gb': 33,
'free_capacity_gb': 22,
'reserved_percentage': 0,
}, ]
self.mock_object(
self._driver,
'_execute',
mock.Mock(return_value=("VSize 33g VFree 22g", None)))
self.assertEqual(expected_result,
self._driver.get_share_server_pools())
def test_copy_volume_error(self):
def _fake_exec(*args, **kwargs):
if 'count=0' in args:
raise exception.ProcessExecutionError()
self.mock_object(self._driver, '_execute',
mock.Mock(side_effect=_fake_exec))
self._driver._copy_volume('src', 'dest', 1)
self._driver._execute.assert_any_call('dd', 'count=0', 'if=src',
'of=dest', 'iflag=direct',
'oflag=direct', run_as_root=True)
self._driver._execute.assert_any_call('dd', 'if=src', 'of=dest',
'count=1024', 'bs=1M',
run_as_root=True)
def test_update_share_stats(self):
self.mock_object(self._driver, 'get_share_server_pools',
mock.Mock(return_value='test-pool'))
self._driver._update_share_stats()
self.assertEqual('LVM', self._driver._stats['share_backend_name'])
self.assertEqual('NFS_CIFS', self._driver._stats['storage_protocol'])
self.assertEqual(50, self._driver._stats['reserved_percentage'])
self.assertEqual(None,
self._driver._stats['consistency_group_support'])
self.assertEqual(True, self._driver._stats['snapshot_support'])
self.assertEqual('LVMShareDriver', self._driver._stats['driver_name'])
self.assertEqual('test-pool', self._driver._stats['pools'])