Container driver

This driver exploits a Docker container as a share server instead
of a virtual machine. The container acts as a CIFS server
and shares logical volume attached to it. The volume gets created
simultaneously to the container on a host in a volume group
specified by the user.

DocImpact
Change-Id: I711813a2022d765ab7a70ed670e34f68f9cf1399
Implements: blueprint manila-container-driver
This commit is contained in:
Alexey Ovchinnikov 2016-04-21 14:02:12 +03:00
parent 729c75cdc5
commit ddf960a2d9
23 changed files with 1844 additions and 19 deletions

View File

@ -207,6 +207,19 @@ elif [[ "$DRIVER" == "dummy" ]]; then
iniset $TEMPEST_CONFIG share multitenancy_enabled True
iniset $TEMPEST_CONFIG share create_networks_when_multitenancy_enabled False
iniset $TEMPEST_CONFIG share multi_backend True
elif [[ "$DRIVER" == "container" ]]; then
MANILA_TEMPEST_CONCURRENCY=1
RUN_MANILA_CG_TESTS=False
RUN_MANILA_MANAGE_TESTS=False
iniset $TEMPEST_CONFIG share run_migration_tests False
iniset $TEMPEST_CONFIG share run_quota_tests False
iniset $TEMPEST_CONFIG share run_replication_tests False
iniset $TEMPEST_CONFIG share run_shrink_tests False
iniset $TEMPEST_CONFIG share run_snapshot_tests False
iniset $TEMPEST_CONFIG share capability_storage_protocol 'CIFS'
iniset $TEMPEST_CONFIG share enable_protocols 'cifs'
iniset $TEMPEST_CONFIG share enable_user_rules_for_protocols 'cifs'
iniset $TEMPEST_CONFIG share enable_ip_rules_for_protocols ''
fi
# Enable consistency group tests

View File

@ -135,6 +135,10 @@ elif [[ "$DRIVER" == "zfsonlinux" ]]; then
# the build timeout for ZFS on the gate.
echo "MANILA_REPLICA_STATE_UPDATE_INTERVAL=60" >> $localrc_path
echo "MANILA_ZFSONLINUX_USE_SSH=True" >> $localrc_path
elif [[ "$DRIVER" == "container" ]]; then
echo "SHARE_DRIVER=manila.share.drivers.container.driver.ContainerShareDriver" >> $localrc_path
echo "SHARE_BACKING_FILE_SIZE=32000M" >> $localrc_path
echo "MANILA_DEFAULT_SHARE_TYPE_EXTRA_SPECS='snapshot_support=false'" >> $localrc_path
fi
echo "MANILA_SERVICE_IMAGE_ENABLED=$MANILA_SERVICE_IMAGE_ENABLED" >> $localrc_path

View File

@ -271,6 +271,18 @@ function create_manila_service_keypair {
}
function is_driver_enabled {
driver_name=$1
for BE in ${MANILA_ENABLED_BACKENDS//,/ }; do
share_driver=$(iniget $MANILA_CONF $BE share_driver)
if [ "$share_driver" == "$driver_name" ]; then
return 0
fi
done
return 1
}
# create_service_share_servers - creates service Nova VMs, one per generic
# driver, and only if it is configured to mode without handling of share servers.
function create_service_share_servers {
@ -443,7 +455,13 @@ function create_default_share_type {
local type_exists=$( manila type-list | grep " $MANILA_DEFAULT_SHARE_TYPE " )
if [[ -z $type_exists ]]; then
manila type-create $MANILA_DEFAULT_SHARE_TYPE $driver_handles_share_servers
local command_args="$MANILA_DEFAULT_SHARE_TYPE $driver_handles_share_servers"
#if is_driver_enabled $MANILA_CONTAINER_DRIVER; then
# # TODO(aovchinnikov): Remove this condition when Container driver supports
# # snapshots
# command_args="$command_args --snapshot_support false"
#fi
manila type-create $command_args
fi
if [[ $MANILA_DEFAULT_SHARE_TYPE_EXTRA_SPECS ]]; then
manila type-key $MANILA_DEFAULT_SHARE_TYPE set $MANILA_DEFAULT_SHARE_TYPE_EXTRA_SPECS
@ -451,6 +469,25 @@ function create_default_share_type {
}
# configure_backing_file - Set up backing file for LVM
function configure_backing_file {
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
mkdir -p /tmp/shares
}
# init_manila - Initializes database and creates manila dir if absent
function init_manila {
@ -480,21 +517,15 @@ function init_manila {
#
# 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
configure_backing_file
fi
elif [ "$SHARE_DRIVER" == $MANILA_CONTAINER_DRIVER ]; then
if is_service_enabled m-shr; then
SHARE_GROUP=$MANILA_CONTAINER_VOLUME_GROUP_NAME
iniset $MANILA_CONF DEFAULT neutron_host_id $(hostname)
configure_backing_file
fi
elif [ "$SHARE_DRIVER" == "manila.share.drivers.zfsonlinux.driver.ZFSonLinuxShareDriver" ]; then
if is_service_enabled m-shr; then
mkdir -p $MANILA_ZFSONLINUX_BACKEND_FILES_CONTAINER_DIR
@ -591,6 +622,20 @@ function install_manila {
exit 1
fi
fi
elif [ "$SHARE_DRIVER" == $MANILA_CONTAINER_DRIVER ]; then
if is_service_enabled m-shr; then
echo "m-shr service is enabled"
if is_ubuntu; then
echo "Installing docker...."
install_docker_ubuntu
echo "Importing docker image"
import_docker_service_image_ubuntu
else
echo "Manila Devstack plugin does not support Container Driver on "\
" distros other than Ubuntu."
exit 1
fi
fi
fi
# install manila-ui if horizon is enabled
@ -688,6 +733,58 @@ function update_tempest {
fi
}
function install_docker_ubuntu {
sudo apt-get update
install_package apparmor
install_package docker.io
}
function download_image {
local image_url=$1
local image image_fname
image_fname=`basename "$image_url"`
if [[ $image_url != file* ]]; then
# Downloads the image (uec ami+akistyle), then extracts it.
if [[ ! -f $FILES/$image_fname || "$(stat -c "%s" $FILES/$image_fname)" = "0" ]]; then
wget --progress=dot:giga -c $image_url -O $FILES/$image_fname
if [[ $? -ne 0 ]]; then
echo "Not found: $image_url"
return
fi
fi
image="$FILES/${image_fname}"
else
# File based URL (RFC 1738): ``file://host/path``
# Remote files are not considered here.
# unix: ``file:///home/user/path/file``
# windows: ``file:///C:/Documents%20and%20Settings/user/path/file``
image=$(echo $image_url | sed "s/^file:\/\///g")
if [[ ! -f $image || "$(stat -c "%s" $image)" == "0" ]]; then
echo "Not found: $image_url"
return
fi
fi
}
function import_docker_service_image_ubuntu {
GZIPPED_IMG_NAME=`basename "$MANILA_DOCKER_IMAGE_URL"`
IMG_NAME_LOAD=${GZIPPED_IMG_NAME%.*}
LOCAL_IMG_NAME=${IMG_NAME_LOAD%.*}
if [[ "$(sudo docker images -q $LOCAL_IMG_NAME)" == "" ]]; then
download_image $MANILA_DOCKER_IMAGE_URL
# Import image in Docker
gzip -d $FILES/$GZIPPED_IMG_NAME
sudo docker load --input $FILES/$IMG_NAME_LOAD
fi
}
function remove_docker_service_image {
sudo docker rmi $MANILA_DOCKER_IMAGE_ALIAS
}
function install_libraries {
if [ $(trueorfalse False MANILA_MULTI_BACKEND) == True ]; then
if [ $(trueorfalse True RUN_MANILA_MIGRATION_TESTS) == True ]; then

View File

@ -158,6 +158,14 @@ MANILA_ZFSONLINUX_SSH_USERNAME=${MANILA_ZFSONLINUX_SSH_USERNAME:-$STACK_USER}
# Manila will consider replication feature as disabled for ZFSonLinux share driver.
MANILA_ZFSONLINUX_REPLICATION_DOMAIN=${MANILA_ZFSONLINUX_REPLICATION_DOMAIN:-"ZFSonLinux"}
# Container Driver
MANILA_CONTAINER_DRIVER=${MANILA_CONTAINER_DRIVER:-"manila.share.drivers.container.driver.ContainerShareDriver"}
MANILA_DOCKER_IMAGE_ALIAS=${MANILA_DOCKER_IMAGE_ALIAS:-"manila_docker_image"}
MANILA_CONTAINER_VOLUME_GROUP_NAME=${MANILA_CONTAINER_VOLUME_GROUP_NAME:-"manila_docker_volumes"}
# (aovchinnikov): This location is temporary and will be changed to a
# permanent one as soon as possible.
MANILA_DOCKER_IMAGE_URL=${MANILA_DOCKER_IMAGE_URL:-"https://github.com/a-ovchinnikov/manila-image-elements-lxd-images/releases/download/0.1.0/manila-docker-container.tar.gz"}
# Enable manila services
# ----------------------
# We have to add Manila to enabled services for screen_it to work

View File

@ -0,0 +1,127 @@
..
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.
Container Driver
================
The Container driver provides a lightweight solution for share servers
management. It allows to use Docker containers for hosting userspace
shared file systems services.
Supported operations
--------------------
- Create CIFS share;
- Delete CIFS share;
- Allow user access to CIFS share;
- Deny user access to CIFS share;
- Extend CIFS share.
Restrictions
------------
- Current implementation has been tested only on Ubuntu. Devstack plugin won't
work on other distributions however it should be possible to install
prerequisits and set the driver up manually;
- The only supported protocol is CIFS;
- The following features are not implemented:
* Manage/unmanage share;
* Shrink share;
* Create/delete snapshots;
* Create a share fron a snapshot;
* Manage/unmanage snapshots.
Known problems
--------------
- May demonstrate unstable behaviour when running concurrently. It is strongly
suggested that the driver should be used with extreme care in cases
other than building lightweight development and testing environments.
Setting up container driver with devstack
=========================================
The driver could be set up via devstack. This requires the following update to
local.conf:
.. code-block:: ini
enable_plugin manila https://git.openstack.org/openstack/manila <ref>
MANILA_BACKEND1_CONFIG_GROUP_NAME=london
MANILA_SHARE_BACKEND1_NAME=LONDON
MANILA_OPTGROUP_london_driver_handles_share_servers=True
MANILA_OPTGROUP_london_neutron_host_id=<hostname>
SHARE_DRIVER=manila.share.drivers.container.driver.ContainerShareDriver
SHARE_BACKING_FILE_SIZE=<backing file size>
MANILA_DEFAULT_SHARE_TYPE_EXTRA_SPECS='snapshot_support=false'
where <ref> is change reference, which could be copied from gerrit web-interface,
<hostname> is the name of the host with running neutron
Setting Container Driver Up Manually
====================================
This section describes steps needed to be performed to set the driver up
manually. The driver has been tested on Ubuntu 14.04, thus in case of
any other distribution package names might differ.
The following packages must be installed:
- docker.io
One can verify if the package is installed by issuing ``sudo docker info``
command. In case of normal operation it should return docker usage statistics.
In case it fails complaining on inaccessible socket try installing
``apparmor``. Please note that docker usage requires superuser privileges.
After docker is successfully installed a docker image containing necessary
packages must be provided. Currently such image could be downloaded from
https://github.com/a-ovchinnikov/manila-image-elements-lxd-images/releases/download/0.1.0/manila-docker-container.tar.gz.
The image has to be unpacked but not untarred. This could be achieved by
running 'gzip -d <imagename>' command. Resulting tar-archive of the
image could be uploaded to docker via
.. code-block:: console
sudo docker load --input <imagename.tar>
If the previous command finished successfully you will be able to see the image
in the image list:
.. code-block:: console
sudo docker images
The driver expects to find a folder /tmp/shares on the host where it is running
as well as a logical volume group "manila_docker_volumes".
When installing the driver manually one must make sure that 'brctl' and
'docker' commands are present in the /etc/manila/rootwrap.d/share.filters
and could be executed as root.
Finally to use the driver one must add a backend to the config file
containing the following settings:
.. code-block:: ini
driver_handles_share_servers = True
share_driver = manila.share.drivers.container.driver.ContainerShareDriver
neutron_host_id = <hostname>
where <hostname> is the name of the host running neutron. (In case of single
VM devstack it is VM's name).
After restarting manila services you should be able to use the driver.

View File

@ -104,6 +104,7 @@ Share backends
.. toctree::
:maxdepth: 3
container_driver
zfs_on_linux_driver
netapp_cluster_mode_driver
emc_isilon_driver

View File

@ -35,6 +35,8 @@ Mapping of share drivers and share features support
+========================================+=======================+=======================+==============+==============+========================+============================+==========================+
| ZFSonLinux | M | \- | M | M | M | M | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Container | N | \- | N | \- | \- | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Generic (Cinder as back-end) | J | K | L | L | J | J | M |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| NetApp Clustered Data ONTAP | J | L | L | L | J | J | N |
@ -80,6 +82,8 @@ Mapping of share drivers and share access rules support
+========================================+==============+================+============+==============+==============+================+============+============+
| ZFSonLinux | NFS (M) | \- | \- | \- | NFS (M) | \- | \- | \- |
+----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+
| Container | \- | CIFS (N) | \- | \- | \- | CIFS (N) | \- | \- |
+----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+
| Generic (Cinder as back-end) | NFS,CIFS (J) | \- | \- | \- | NFS (K) | \- | \- | \- |
+----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+
| NetApp Clustered Data ONTAP | NFS (J) | CIFS (J) | \- | \- | NFS (K) | CIFS (M) | \- | \- |
@ -123,6 +127,8 @@ Mapping of share drivers and security services support
+========================================+==================+=================+==================+
| ZFSonLinux | \- | \- | \- |
+----------------------------------------+------------------+-----------------+------------------+
| Container | \- | \- | \- |
+----------------------------------------+------------------+-----------------+------------------+
| Generic (Cinder as back-end) | \- | \- | \- |
+----------------------------------------+------------------+-----------------+------------------+
| NetApp Clustered Data ONTAP | J | J | J |
@ -166,6 +172,8 @@ Mapping of share drivers and common capabilities
+========================================+===========+============+========+=============+===================+====================+=====+
| ZFSonLinux | \- | M | M | M | M | \- | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+
| Container | N | \- | \- | \- | \- | N | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+
| Generic (Cinder as back-end) | J | K | \- | \- | \- | L | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+
| NetApp Clustered Data ONTAP | J | K | M | M | M | L | \- |

View File

@ -151,3 +151,12 @@ ls: CommandFilter, ls, root
# manila/data/utils.py: 'touch', '--reference=%s', '%s'
touch: CommandFilter, touch, root
# manila/share/drivers/container/container.py: docker <whatever>
docker: CommandFilter, docker, root
# manila/share/drivers/container/container.py: brctl <whatever>
brctl: CommandFilter, brctl, root
# manila/share/drivers/container/container.py: e2fsck <whatever>
e2fsck: CommandFilter, e2fsck, root

View File

@ -39,6 +39,14 @@ neutron_single_network_plugin_opts = [
deprecated_group='DEFAULT'),
]
neutron_network_plugin_opts = [
cfg.StrOpt(
"neutron_host_id",
help="Host ID to be used when creating neutron port. Hostname of "
"a controller running Neutron should be used in a general case.",
deprecated_group='DEFAULT'),
]
CONF = cfg.CONF
@ -51,6 +59,9 @@ class NeutronNetworkPlugin(network.NetworkBaseAPI):
self._neutron_api_args = args
self._neutron_api_kwargs = kwargs
self._label = kwargs.pop('label', 'user')
CONF.register_opts(
neutron_network_plugin_opts,
group=self.neutron_api.config_group_name)
@property
def label(self):
@ -114,11 +125,12 @@ class NeutronNetworkPlugin(network.NetworkBaseAPI):
self._delete_port(context, port)
def _create_port(self, context, share_server, share_network, device_owner):
host_id = self.neutron_api.configuration.neutron_host_id
port = self.neutron_api.create_port(
share_network['project_id'],
network_id=share_network['neutron_net_id'],
subnet_id=share_network['neutron_subnet_id'],
device_owner='manila:' + device_owner)
device_owner='manila:' + device_owner, host_id=host_id)
port_dict = {
'id': port['id'],
'share_server_id': share_server['id'],

View File

@ -51,6 +51,8 @@ import manila.service
import manila.share.api
import manila.share.driver
import manila.share.drivers.cephfs.cephfs_native
import manila.share.drivers.container.driver
import manila.share.drivers.container.storage_helper
import manila.share.drivers.emc.driver
import manila.share.drivers.emc.plugins.isilon.isilon
import manila.share.drivers.generic
@ -99,6 +101,8 @@ _global_opt_lists = [
manila.network.network_opts,
manila.network.neutron.api.neutron_opts,
manila.network.neutron.neutron_network_plugin.
neutron_network_plugin_opts,
manila.network.neutron.neutron_network_plugin.
neutron_single_network_plugin_opts,
manila.network.nova_network_plugin.nova_single_network_plugin_opts,
manila.network.standalone_network_plugin.standalone_network_plugin_opts,
@ -117,6 +121,8 @@ _global_opt_lists = [
manila.share.driver.ssh_opts,
manila.share.drivers_private_data.private_data_opts,
manila.share.drivers.cephfs.cephfs_native.cephfs_native_opts,
manila.share.drivers.container.driver.container_opts,
manila.share.drivers.container.storage_helper.lv_opts,
manila.share.drivers.emc.driver.EMC_NAS_OPTS,
manila.share.drivers.generic.share_opts,
manila.share.drivers.glusterfs.common.glusterfs_common_opts,

View File

@ -0,0 +1,79 @@
# Copyright (c) 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.
import uuid
from oslo_log import log
from manila import exception
from manila.i18n import _
from manila.i18n import _LI
from manila.share import driver
LOG = log.getLogger(__name__)
class DockerExecHelper(driver.ExecuteMixin):
def __init__(self, *args, **kwargs):
self.configuration = kwargs.pop("configuration", None)
super(DockerExecHelper, self).__init__(*args, **kwargs)
self.init_execute_mixin()
def start_container(self, name=None):
name = name or "".join(["manila_cifs_docker_container",
str(uuid.uuid1()).replace("-", "_")])
image_name = self.configuration.container_image_name
LOG.debug("Starting container from image %s.", image_name)
# (aovchinnikov): --privileged is required for both samba and
# nfs-ganesha to actually allow access to shared folders.
cmd = ["docker", "run", "-d", "-i", "-t", "--privileged",
"--name=%s" % name, '-v', "/tmp/shares:/shares", image_name]
result = self._inner_execute(cmd) or ['', 1]
if result[1] != '':
raise exception.ManilaException(
_("Container %s has failed to start.") % name)
LOG.info(_LI("A container has been successfully started! Its id is "
"%s."), result[0].rstrip('\n'))
def stop_container(self, name):
LOG.debug("Stopping container %s.", name)
cmd = ["docker", "stop", name]
result = self._inner_execute(cmd) or ['', 1]
if result[1] != '':
raise exception.ManilaException(
_("Container %s has failed to stop properly.") % name)
LOG.info(_LI("Container %s is successfully stopped."), name)
def execute(self, name=None, cmd=None):
if name is None:
raise exception.ManilaException(_("Container name not specified."))
if cmd is None or (type(cmd) is not list):
raise exception.ManilaException(_("Missing or malformed command."))
LOG.debug("Executing inside a container %s.", name)
cmd = ["docker", "exec", "-i", name] + cmd
result = self._inner_execute(cmd)
LOG.debug("Run result: %s.", str(result))
return result
def _inner_execute(self, cmd):
LOG.debug("Executing command: %s.", " ".join(cmd))
try:
result = self._execute(*cmd, run_as_root=True)
except Exception as e:
LOG.exception(e)
return None
LOG.debug("Execution result: %s.", result)
return result

View File

@ -0,0 +1,282 @@
# Copyright (c) 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.
"""Container Driver for shares.
This driver uses a container as a share server.
Current implementation suggests that a container when started by Docker will
be plugged into a Linux bridge. Also it is suggested that all interfaces
willing to talk to each other reside in an OVS bridge."""
import re
from oslo_config import cfg
from oslo_log import log
from oslo_utils import importutils
from manila import exception
from manila.i18n import _
from manila.i18n import _LI
from manila.i18n import _LW
from manila.share import driver
from manila import utils
CONF = cfg.CONF
LOG = log.getLogger(__name__)
container_opts = [
cfg.StrOpt("container_linux_bridge_name",
default="docker0",
required=True,
help="Linux bridge used by container hypervisor to plug "
"host-side veth to. It will be unplugged from here "
"by the driver."),
cfg.StrOpt("container_ovs_bridge_name",
default="br-int",
required=True,
help="OVS bridge to use to plug a container to."),
cfg.BoolOpt("container_cifs_guest_ok",
default=True,
help="Determines whether to allow guest access to CIFS share "
"or not."),
cfg.StrOpt("container_image_name",
default="manila-docker-container",
help="Image to be used for a container-based share server."),
cfg.StrOpt("container_helper",
default="manila.share.drivers.container.container_helper."
"DockerExecHelper",
help="Container helper which provides container-related "
"operations to the driver."),
cfg.StrOpt("container_protocol_helper",
default="manila.share.drivers.container.protocol_helper."
"DockerCIFSHelper",
help="Helper which facilitates interaction with share server."),
cfg.StrOpt("container_storage_helper",
default="manila.share.drivers.container.storage_helper."
"LVMHelper",
help="Helper which facilitates interaction with storage "
"solution used to actually store data. By default LVM "
"is used to provide storage for a share."),
]
class ContainerShareDriver(driver.ShareDriver, driver.ExecuteMixin):
def __init__(self, *args, **kwargs):
super(ContainerShareDriver, self).__init__([True], *args, **kwargs)
self.configuration.append_config_values(container_opts)
self.backend_name = self.configuration.safe_get(
"share_backend_name") or "Docker"
self.container = importutils.import_class(
self.configuration.container_helper)(
configuration=self.configuration)
self.storage = importutils.import_class(
self.configuration.container_storage_helper)(
configuration=self.configuration)
self._helpers = {}
def _get_helper(self, share):
if share["share_proto"].upper() == "CIFS":
helper = self._helpers.get("CIFS")
if helper is not None:
return helper(self.container,
share=share,
config=self.configuration)
self._helpers["CIFS"] = importutils.import_class(
self.configuration.container_protocol_helper)
return self._helpers["CIFS"](self.container,
share=share,
config=self.configuration)
else:
raise exception.InvalidShare(
reason=_("Wrong, unsupported or disabled protocol."))
def _update_share_stats(self):
data = {
'share_backend_name': self.backend_name,
'storage_protocol': 'CIFS',
'reserved_percentage':
self.configuration.reserved_share_percentage,
'consistency_group_support': None,
'snapshot_support': False,
'driver_name': 'ContainerShareDriver',
'pools': self.storage.get_share_server_pools()
}
super(ContainerShareDriver, self)._update_share_stats(data)
def create_share(self, context, share, share_server=None):
LOG.debug("Create share on server '%s'." % share_server["id"])
server_id = self._get_container_name(share_server["id"])
share_name = share.share_id
self.container.execute(
server_id,
["mkdir", "-m", "750", "/shares/%s" % share_name]
)
self.storage.provide_storage(share)
location = self._get_helper(share).create_share(server_id)
return location
def delete_share(self, context, share, share_server=None):
LOG.debug("Deleting share %(share)s on server '%(server)s'." %
{"server": share_server["id"],
"share": share.share_id})
server_id = self._get_container_name(share_server["id"])
self._get_helper(share).delete_share(server_id)
self.storage.remove_storage(share)
self.container.execute(
server_id,
["rm", "-fR", "/shares/%s" % share.share_id]
)
LOG.debug("Deletion of share %s is completed!", share.share_id)
def extend_share(self, share, new_size, share_server=None):
self.storage.extend_share(share, new_size, share_server)
def ensure_share(self, context, share, share_server=None):
pass
def update_access(self, context, share, access_rules, add_rules,
delete_rules, share_server=None):
server_id = self._get_container_name(share_server["id"])
LOG.debug("Updating access to share %(share)s at "
"share server %(share_server)s.",
{"share_server": share_server["id"],
"share": share.share_id})
self._get_helper(share).update_access(server_id,
access_rules, add_rules,
delete_rules)
def get_network_allocations_number(self):
return 1
def _get_container_name(self, server_id):
return "manila_%s" % server_id.replace("-", "_")
def do_setup(self, *args, **kwargs):
pass
def check_for_setup_error(self, *args, **kwargs):
host_id = self.configuration.safe_get("neutron_host_id")
neutron_class = importutils.import_class(
'manila.network.neutron.neutron_network_plugin.'
'NeutronNetworkPlugin'
)
actual_class = importutils.import_class(
self.configuration.safe_get("network_api_class"))
if host_id is None and issubclass(actual_class, neutron_class):
msg = _("%s requires neutron_host_id to be "
"specified.") % neutron_class
raise exception.ManilaException(msg)
elif host_id is None:
LOG.warning(_LW("neutron_host_id is not specified. This driver "
"might not work as expected without it."))
def _connect_to_network(self, server_id, network_info, host_veth):
LOG.debug("Attempting to connect container to neutron network.")
network_allocation = network_info['network_allocations'][0]
port_address = network_allocation.ip_address
port_mac = network_allocation.mac_address
port_id = network_allocation.id
self.container.execute(
server_id,
["ifconfig", "eth0", port_address, "up"]
)
self.container.execute(
server_id,
["ip", "link", "set", "dev", "eth0", "address", port_mac]
)
msg_helper = {
'id': server_id, 'veth': host_veth,
'lb': self.configuration.container_linux_bridge_name,
'ovsb': self.configuration.container_ovs_bridge_name,
'ip': port_address,
'subnet': network_info['neutron_subnet_id'],
}
LOG.debug("Container %(id)s veth is %(veth)s.", msg_helper)
LOG.debug("Removing %(veth)s from %(lb)s.", msg_helper)
self._execute("brctl", "delif",
self.configuration.container_linux_bridge_name,
host_veth,
run_as_root=True)
LOG.debug("Plugging %(veth)s into %(ovsb)s.", msg_helper)
set_if = ['--', 'set', 'interface', host_veth]
e_mac = set_if + ['external-ids:attached-mac="%s"' % port_mac]
e_id = set_if + ['external-ids:iface-id="%s"' % port_id]
e_status = set_if + ['external-ids:iface-status=active']
e_mcid = set_if + ['external-ids:manila-container=%s' % server_id]
self._execute("ovs-vsctl", "--", "add-port",
self.configuration.container_ovs_bridge_name, host_veth,
*(e_mac + e_id + e_status + e_mcid), run_as_root=True)
LOG.debug("Now container %(id)s should be accessible from network "
"%(subnet)s by address %(ip)s." % msg_helper)
def _teardown_server(self, *args, **kwargs):
server_id = self._get_container_name(kwargs["server_details"]["id"])
self.container.stop_container(server_id)
interfaces = self._execute("ovs-vsctl", "list", "interface",
run_as_root=True)[0]
veths = re.findall("veth[0-9a-zA-Z]{7}", interfaces)
manila_re = ("manila_[0-9a-f]{8}_[0-9a-f]{4}_[0-9a-f]{4}_[0-9a-f]{4}_"
"[0-9a-f]{12}")
for veth in veths:
iface_data = self._execute("ovs-vsctl", "list", "interface", veth,
run_as_root=True)[0]
container_id = re.findall(manila_re, iface_data)
if container_id == []:
continue
elif container_id[0] == server_id:
LOG.debug("Deleting veth %s.", veth)
self._execute("ovs-vsctl", "--", "del-port",
self.configuration.container_ovs_bridge_name,
veth, run_as_root=True)
def _get_veth_state(self):
result = self._execute("brctl", "show",
self.configuration.container_linux_bridge_name,
run_as_root=True)
veths = re.findall("veth.*\\n", result[0])
veths = [x.rstrip('\n') for x in veths]
msg = ("The following veth interfaces are plugged into %s now: " %
self.configuration.container_linux_bridge_name)
LOG.debug(msg + ", ".join(veths))
return veths
def _get_corresponding_veth(self, before, after):
result = list(set(after) ^ set(before))
if len(result) != 1:
raise exception.ManilaException(_("Multiple veths for container."))
return result[0]
@utils.synchronized("veth-lock", external=True)
def _setup_server(self, network_info, metadata=None):
msg = "Creating share server '%s'."
server_id = self._get_container_name(network_info["server_id"])
LOG.debug(msg % server_id)
veths_before = self._get_veth_state()
try:
self.container.start_container(server_id)
except Exception as e:
raise exception.ManilaException(_("Cannot create container: %s") %
e)
veths_after = self._get_veth_state()
veth = self._get_corresponding_veth(veths_before, veths_after)
self._connect_to_network(server_id, network_info, veth)
LOG.info(_LI("Container %s was created."), server_id)
return {"id": network_info["server_id"]}

View File

@ -0,0 +1,144 @@
# Copyright (c) 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.
import re
from oslo_log import log
from manila.common import constants as const
from manila import exception
from manila.i18n import _
from manila.i18n import _LW
LOG = log.getLogger(__name__)
class DockerCIFSHelper(object):
def __init__(self, container_helper, *args, **kwargs):
super(DockerCIFSHelper, self).__init__()
self.share = kwargs.get("share")
self.conf = kwargs.get("config")
self.container = container_helper
def create_share(self, server_id):
share_name = self.share.share_id
cmd = ["net", "conf", "addshare", share_name,
"/shares/%s" % share_name, "writeable=y"]
if self.conf.container_cifs_guest_ok:
cmd.append("guest_ok=y")
else:
cmd.append("guest_ok=n")
self.container.execute(server_id, cmd)
parameters = {
"browseable": "yes",
"create mask": "0755",
"read only": "no",
}
for param, value in parameters.items():
self.container.execute(
server_id,
["net", "conf", "setparm", share_name, param, value]
)
result = self.container.execute(
server_id,
["ip", "addr", "show", "eth0"]
)[0].split('\n')[2]
address = re.findall("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", result)[0]
return r"//%(addr)s/%(name)s" % {"addr": address, "name": share_name}
def delete_share(self, server_id):
self.container.execute(
server_id,
["net", "conf", "delshare", self.share.share_id]
)
def _get_access_group(self, access_level):
if access_level == const.ACCESS_LEVEL_RO:
access = "read list"
elif access_level == const.ACCESS_LEVEL_RW:
access = "valid users"
else:
raise exception.InvalidShareAccessLevel(level=access_level)
return access
def _get_existing_users(self, server_id, share_name, access):
result = self.container.execute(
server_id,
["net", "conf", "getparm", share_name, access]
)[0].rstrip('\n')
return result
def _set_users(self, server_id, share_name, access, users_to_set):
self.container.execute(
server_id,
["net", "conf", "setparm", share_name, access, users_to_set]
)
def _allow_access(self, share_name, server_id, user_to_allow,
access_level):
access = self._get_access_group(access_level)
try:
existing_users = self._get_existing_users(server_id, share_name,
access)
except TypeError:
users_to_allow = user_to_allow
else:
users_to_allow = " ".join([existing_users, user_to_allow])
self._set_users(server_id, share_name, access, users_to_allow)
def _deny_access(self, share_name, server_id, user_to_deny,
access_level):
access = self._get_access_group(access_level)
try:
existing_users = self._get_existing_users(server_id, share_name,
access)
except TypeError:
LOG.warning(_LW("Can't access smbd at share %s.") % share_name)
return
else:
allowed_users = " ".join(sorted(set(existing_users.split()) -
set([user_to_deny])))
if allowed_users != existing_users:
self._set_users(server_id, share_name, access, allowed_users)
def update_access(self, server_id, access_rules,
add_rules=None, delete_rules=None):
def _rule_updater(rules, action, override_type_check=False):
for rule in rules:
access_level = rule['access_level']
access_type = rule['access_type']
# (aovchinnikov): override_type_check is used to ensure
# broken rules deletion.
if access_type == 'user' or override_type_check:
action(share_name, server_id, rule['access_to'],
access_level)
else:
msg = _("Access type '%s' is not supported by the "
"driver.") % access_type
raise exception.InvalidShareAccess(reason=msg)
share_name = self.share.share_id
if not (add_rules or delete_rules):
# clean all users first.
self.container.execute(
server_id,
["net", "conf", "setparm", share_name, "valid users", ""]
)
_rule_updater(access_rules or [], self._allow_access)
return
_rule_updater(add_rules or [], self._allow_access)
_rule_updater(delete_rules or [], self._deny_access,
override_type_check=True)

View File

@ -0,0 +1,100 @@
# Copyright (c) 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.
import os
import re
from oslo_config import cfg
from manila import exception
from manila.i18n import _
from manila.share import driver
CONF = cfg.CONF
lv_opts = [
cfg.StrOpt("container_volume_group",
default="manila_docker_volumes",
help="LVM volume group to use for volumes. This volume group "
"must be created by the cloud administrator independently "
"from manila operations."),
]
CONF.register_opts(lv_opts)
class LVMHelper(driver.ExecuteMixin):
def __init__(self, *args, **kwargs):
self.configuration = kwargs.pop("configuration", None)
if self.configuration is None:
raise exception.ManilaException(_("LVMHelper called without "
"supplying configuration."))
self.configuration.append_config_values(lv_opts)
super(LVMHelper, self).__init__(*args, **kwargs)
self.init_execute_mixin()
def get_share_server_pools(self, share_server=None):
out, err = self._execute('vgs',
self.configuration.container_volume_group,
'--rows', run_as_root=True)
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': self.configuration.container_volume_group,
'total_capacity_gb': float(total_size),
'free_capacity_gb': float(free_size),
'reserved_percentage': 0,
}, ]
def _get_lv_device(self, share):
return os.path.join("/dev", self.configuration.container_volume_group,
share.share_id)
def _get_lv_folder(self, share):
# Provides folder name in hosts /tmp to which logical volume is
# mounted prior to providing access to it from a container.
return os.path.join("/tmp/shares", share.share_id)
def provide_storage(self, share):
share_name = share.share_id
self._execute("lvcreate", "-p", "rw", "-L",
str(share.size) + "G", "-n", share_name,
self.configuration.container_volume_group,
run_as_root=True)
self._execute("mkfs.ext4", self._get_lv_device(share),
run_as_root=True)
self._execute("mount", self._get_lv_device(share),
self._get_lv_folder(share), run_as_root=True)
self._execute("chmod", "-R", "750", self._get_lv_folder(share),
run_as_root=True)
self._execute("chown", "nobody:nogroup", self._get_lv_folder(share),
run_as_root=True)
def remove_storage(self, share):
self._execute("umount", self._get_lv_device(share), run_as_root=True)
self._execute("lvremove", "-f", "--autobackup", "n",
self._get_lv_device(share), run_as_root=True)
def extend_share(self, share, new_size, share_server=None):
lv_device = self._get_lv_device(share)
lv_folder = self._get_lv_folder(share)
self._execute("umount", lv_folder, run_as_root=True)
cmd = ('lvextend', '-L', '%sG' % new_size, '-n', lv_device)
self._execute(*cmd, run_as_root=True)
self._execute("e2fsck", "-f", "-y", lv_device, run_as_root=True)
self._execute('resize2fs', lv_device, run_as_root=True)
self._execute("mount", lv_device, lv_folder, run_as_root=True)

View File

@ -130,7 +130,7 @@ class NeutronNetworkPluginTest(test.TestCase):
fake_share_network['project_id'],
network_id=fake_share_network['neutron_net_id'],
subnet_id=fake_share_network['neutron_subnet_id'],
device_owner='manila:share')
device_owner='manila:share', host_id=None)
db_api.network_allocation_create.assert_called_once_with(
self.fake_context,
fake_network_allocation)
@ -139,6 +139,54 @@ class NeutronNetworkPluginTest(test.TestCase):
save_nw_data.stop()
save_subnet_data.stop()
@mock.patch.object(db_api, 'network_allocation_create',
mock.Mock(return_values=fake_network_allocation))
@mock.patch.object(db_api, 'share_network_get',
mock.Mock(return_value=fake_share_network))
@mock.patch.object(db_api, 'share_server_get',
mock.Mock(return_value=fake_share_server))
def test_allocate_network_host_id_prconfigured(self):
has_provider_nw_ext = mock.patch.object(
self.plugin, '_has_provider_network_extension').start()
has_provider_nw_ext.return_value = True
save_nw_data = mock.patch.object(self.plugin,
'_save_neutron_network_data').start()
save_subnet_data = mock.patch.object(
self.plugin,
'_save_neutron_subnet_data').start()
config_data = {
'DEFAULT': {
'neutron_host_id': 'fake_host_id',
}
}
with test_utils.create_temp_config_with_opts(config_data):
with mock.patch.object(self.plugin.neutron_api, 'create_port',
mock.Mock(return_value=fake_neutron_port)):
self.plugin.allocate_network(
self.fake_context,
fake_share_server,
fake_share_network,
allocation_info={'count': 1})
has_provider_nw_ext.assert_any_call()
save_nw_data.assert_called_once_with(self.fake_context,
fake_share_network)
save_subnet_data.assert_called_once_with(self.fake_context,
fake_share_network)
self.plugin.neutron_api.create_port.assert_called_once_with(
fake_share_network['project_id'],
network_id=fake_share_network['neutron_net_id'],
subnet_id=fake_share_network['neutron_subnet_id'],
device_owner='manila:share', host_id='fake_host_id')
db_api.network_allocation_create.assert_called_once_with(
self.fake_context,
fake_network_allocation)
has_provider_nw_ext.stop()
save_nw_data.stop()
save_subnet_data.stop()
@mock.patch.object(db_api, 'network_allocation_create',
mock.Mock(return_values=fake_network_allocation))
@mock.patch.object(db_api, 'share_network_get',
@ -167,11 +215,11 @@ class NeutronNetworkPluginTest(test.TestCase):
mock.call(fake_share_network['project_id'],
network_id=fake_share_network['neutron_net_id'],
subnet_id=fake_share_network['neutron_subnet_id'],
device_owner='manila:share'),
device_owner='manila:share', host_id=None),
mock.call(fake_share_network['project_id'],
network_id=fake_share_network['neutron_net_id'],
subnet_id=fake_share_network['neutron_subnet_id'],
device_owner='manila:share'),
device_owner='manila:share', host_id=None),
]
db_api_calls = [
mock.call(self.fake_context, fake_network_allocation),

View File

@ -0,0 +1,56 @@
# 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.
"""Some useful fakes."""
from manila.tests.db import fakes as db_fakes
def fake_share(**kwargs):
share = {
'id': 'fakeid',
'share_id': 'fakeshareid',
'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_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)
def fake_network(**kwargs):
allocations = db_fakes.FakeModel({'id': 'fake_allocation_id',
'ip_address': '127.0.0.0.1',
'mac_address': 'fe:16:3e:61:e0:58'})
network = {
'id': 'fake_network_id',
'server_id': 'fake_server_id',
'network_allocations': [allocations],
'neutron_subnet_id': 'fake_subnet',
}
network.update(kwargs)
return db_fakes.FakeModel(network)

View File

@ -0,0 +1,110 @@
# Copyright (c) 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.
"""Unit tests for the Container helper module."""
import mock
import uuid
from manila import exception
from manila.share import configuration
from manila.share.drivers.container import container_helper
from manila import test
class DockerExecHelperTestCase(test.TestCase):
"""Tests DockerExecHelper"""
def setUp(self):
super(DockerExecHelperTestCase, self).setUp()
self.fake_conf = configuration.Configuration(None)
self.fake_conf.container_image_name = "fake_image"
self.DockerExecHelper = container_helper.DockerExecHelper(
configuration=self.fake_conf)
def test_start_container(self):
self.mock_object(self.DockerExecHelper, "_inner_execute",
mock.Mock(return_value=['fake_output', '']))
uuid.uuid1 = mock.Mock(return_value='')
expected = ['docker', 'run', '-d', '-i', '-t', '--privileged',
'--name=manila_cifs_docker_container', '-v',
'/tmp/shares:/shares', 'fake_image']
self.DockerExecHelper.start_container()
self.DockerExecHelper._inner_execute.assert_called_once_with(expected)
def test_start_container_impossible_failure(self):
self.mock_object(self.DockerExecHelper, "_inner_execute",
mock.Mock(return_value=['', 'but how?!']))
self.assertRaises(exception.ManilaException,
self.DockerExecHelper.start_container)
def test_stop_container(self):
self.mock_object(self.DockerExecHelper, "_inner_execute",
mock.Mock(return_value=['fake_output', '']))
expected = ['docker', 'stop', 'manila-fake-conainer']
self.DockerExecHelper.stop_container("manila-fake-conainer")
self.DockerExecHelper._inner_execute.assert_called_once_with(expected)
def test_stop_container_oh_noes(self):
self.mock_object(self.DockerExecHelper, "_inner_execute",
mock.Mock(return_value=['fake_output',
'fake_problem']))
self.assertRaises(exception.ManilaException,
self.DockerExecHelper.stop_container,
"manila-fake-container")
def test_execute(self):
self.mock_object(self.DockerExecHelper, "_inner_execute",
mock.Mock(return_value='fake_output'))
expected = ['docker', 'exec', '-i', 'fake_container', 'fake_script']
self.DockerExecHelper.execute("fake_container", ["fake_script"])
self.DockerExecHelper._inner_execute.assert_called_once_with(expected)
def test_execute_name_not_there(self):
self.assertRaises(exception.ManilaException,
self.DockerExecHelper.execute,
None, ['do', 'stuff'])
def test_execute_command_not_there(self):
self.assertRaises(exception.ManilaException,
self.DockerExecHelper.execute,
'fake-name', None)
def test_execute_bad_command_format(self):
self.assertRaises(exception.ManilaException,
self.DockerExecHelper.execute,
'fake-name', 'do stuff')
def test__inner_execute_ok(self):
self.DockerExecHelper._execute = mock.Mock(return_value='fake')
result = self.DockerExecHelper._inner_execute("fake_command")
self.assertEqual(result, 'fake')
def test__inner_execute_not_ok(self):
self.DockerExecHelper._execute = mock.Mock(return_value='fake')
self.DockerExecHelper._execute.side_effect = KeyError()
result = self.DockerExecHelper._inner_execute("fake_command")
self.assertEqual(result, None)

View File

@ -0,0 +1,291 @@
# 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.
"""Unit tests for the Container driver module."""
import functools
import mock
from oslo_config import cfg
from manila.common import constants as const
from manila import context
from manila import exception
from manila.share import configuration
from manila.share.drivers.container import driver
from manila.share.drivers.container import protocol_helper
from manila import test
from manila.tests import fake_utils
from manila.tests.share.drivers.container import fakes as cont_fakes
CONF = cfg.CONF
CONF.import_opt('lvm_share_export_ip', 'manila.share.drivers.lvm')
class ContainerShareDriverTestCase(test.TestCase):
"""Tests ContainerShareDriver"""
def setUp(self):
super(ContainerShareDriverTestCase, self).setUp()
fake_utils.stub_out_utils_execute(self)
self._context = context.get_admin_context()
self._db = mock.Mock()
self.fake_conf = configuration.Configuration(None)
CONF.set_default('driver_handles_share_servers', True)
self._driver = driver.ContainerShareDriver(
configuration=self.fake_conf)
self.share = cont_fakes.fake_share()
self.access = cont_fakes.fake_access()
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 test__get_helper_ok(self):
share = cont_fakes.fake_share(share_proto='CIFS')
expected = protocol_helper.DockerCIFSHelper(None)
actual = self._driver._get_helper(share)
self.assertEqual(type(expected), type(actual))
def test__get_helper_existing_ok(self):
share = cont_fakes.fake_share(share_proto='CIFS')
expected = protocol_helper.DockerCIFSHelper
self._driver._helpers = {'CIFS': expected}
actual = self._driver._get_helper(share)
self.assertEqual(expected, type(actual))
def test__get_helper_not_ok(self):
share = cont_fakes.fake_share()
self.assertRaises(exception.InvalidShare, self._driver._get_helper,
share)
def test_update_share_stats(self):
self.mock_object(self._driver.storage, 'get_share_server_pools',
mock.Mock(return_value='test-pool'))
self._driver._update_share_stats()
self.assertEqual('Docker', self._driver._stats['share_backend_name'])
self.assertEqual('CIFS', self._driver._stats['storage_protocol'])
self.assertEqual(0, self._driver._stats['reserved_percentage'])
self.assertIsNone(self._driver._stats['consistency_group_support'])
self.assertEqual(False, self._driver._stats['snapshot_support'])
self.assertEqual('ContainerShareDriver',
self._driver._stats['driver_name'])
self.assertEqual('test-pool', self._driver._stats['pools'])
def test_create_share(self):
helper = mock.Mock()
self.mock_object(helper, 'create_share',
mock.Mock(return_value='export_location'))
self.mock_object(self._driver, "_get_helper",
mock.Mock(return_value=helper))
self.mock_object(self._driver.storage, 'provide_storage')
self.mock_object(self._driver.container, 'execute')
self.assertEqual('export_location',
self._driver.create_share(self._context, self.share,
{'id': 'fake'}))
def test_delete_share(self):
helper = mock.Mock()
self.mock_object(self._driver, "_get_helper",
mock.Mock(return_value=helper))
self.mock_object(self._driver.container, 'execute')
self.mock_object(self._driver.storage, 'remove_storage')
self._driver.delete_share(self._context, self.share, {'id': 'fake'})
self._driver.container.execute.assert_called_with(
'manila_fake',
['rm', '-fR', '/shares/fakeshareid']
)
def test_extend_share(self):
share = cont_fakes.fake_share()
self.mock_object(self._driver.storage, "extend_share")
self._driver.extend_share(share, 2, 'fake-server')
def test_ensure_share(self):
# Does effectively nothing by design.
self.assertEqual(1, 1)
def test_update_access_access_rules_ok(self):
helper = mock.Mock()
self.mock_object(self._driver, "_get_helper",
mock.Mock(return_value=helper))
self._driver.update_access(self._context, self.share,
[{'access_level': const.ACCESS_LEVEL_RW}],
[], [], {"id": "fake"})
helper.update_access.assert_called_with('manila_fake',
[{'access_level': 'rw'}],
[], [])
def test_get_network_allocation_numer(self):
# Does effectively nothing by design.
self.assertEqual(1, self._driver.get_network_allocations_number())
def test__get_container_name(self):
self.assertEqual("manila_fake_server",
self._driver._get_container_name("fake-server"))
def test_do_setup(self):
# Does effectively nothing by design.
self.assertEqual(1, 1)
def test_check_for_setup_error_host_not_ok_class_ok(self):
setattr(self._driver.configuration.local_conf,
'neutron_host_id', None)
self.assertRaises(exception.ManilaException,
self._driver.check_for_setup_error)
def test_check_for_setup_error_host_not_ok_class_some_other(self):
setattr(self._driver.configuration.local_conf,
'neutron_host_id', None)
setattr(self._driver.configuration.local_conf,
'network_api_class',
'manila.share.drivers.container.driver.ContainerShareDriver')
driver.LOG.warning = mock.Mock()
self._driver.check_for_setup_error()
setattr(self._driver.configuration.local_conf,
'network_api_class',
'manila.network.neutron.neutron_network_plugin.'
'NeutronNetworkPlugin')
self.assertTrue(driver.LOG.warning.called)
def test__connect_to_network(self):
network_info = cont_fakes.fake_network()
helper = mock.Mock()
self.mock_object(self._driver, "_execute",
mock.Mock(return_value=helper))
self.mock_object(self._driver.container, "execute")
self._driver._connect_to_network("fake-server", network_info,
"fake-veth")
def test__teardown_server(self):
def fake_ovs_execute(*args, **kwargs):
kwargs['arguments'].append(args)
if len(args) == 3:
return ['veth0000000']
elif len(args) == 4:
return ('fake:manila_b5afb5c1_6011_43c4_8a37_29820e6951a7', '')
else:
return 0
actual_arguments = []
expected_arguments = [
('ovs-vsctl', 'list', 'interface'),
('ovs-vsctl', 'list', 'interface', 'veth0000000'),
('ovs-vsctl', '--', 'del-port', 'br-int', 'veth0000000')
]
self.mock_object(self._driver.container, "stop_container", mock.Mock())
self._driver._execute = functools.partial(
fake_ovs_execute, arguments=actual_arguments)
self._driver._teardown_server(
server_details={"id": "b5afb5c1-6011-43c4-8a37-29820e6951a7"})
self.assertEqual(expected_arguments.sort(), actual_arguments.sort())
def test__teardown_server_check_continuation(self):
def fake_ovs_execute(*args, **kwargs):
kwargs['arguments'].append(args)
if len(args) == 3:
return ['veth0000000']
elif len(args) == 4:
return ('fake:', '')
else:
return 0
actual_arguments = []
expected_arguments = [
('ovs-vsctl', 'list', 'interface'),
('ovs-vsctl', 'list', 'interface', 'veth0000000'),
('ovs-vsctl', '--', 'del-port', 'br-int', 'veth0000000')
]
self.mock_object(self._driver.container, "stop_container", mock.Mock())
self._driver._execute = functools.partial(
fake_ovs_execute, arguments=actual_arguments)
self._driver._teardown_server(
server_details={"id": "b5afb5c1-6011-43c4-8a37-29820e6951a7"})
self.assertEqual(expected_arguments.sort(), actual_arguments.sort())
def test__get_veth_state(self):
retval = ('veth0000000\n', '')
self.mock_object(self._driver, "_execute",
mock.Mock(return_value=retval))
result = self._driver._get_veth_state()
self.assertEqual(['veth0000000'], result)
def test__get_corresponding_veth_ok(self):
before = ['veth0000000']
after = ['veth0000000', 'veth0000001']
result = self._driver._get_corresponding_veth(before, after)
self.assertEqual('veth0000001', result)
def test__get_corresponding_veth_raises(self):
before = ['veth0000000']
after = ['veth0000000', 'veth0000001', 'veth0000002']
self.assertRaises(exception.ManilaException,
self._driver._get_corresponding_veth,
before, after)
def test__setup_server_container_fails(self):
network_info = cont_fakes.fake_network()
self.mock_object(self._driver.container, 'start_container')
self._driver.container.start_container.side_effect = KeyError()
self.assertRaises(exception.ManilaException,
self._driver._setup_server, network_info)
def test__setup_server_ok(self):
network_info = cont_fakes.fake_network()
server_id = self._driver._get_container_name(network_info["server_id"])
self.mock_object(self._driver.container, 'start_container')
self.mock_object(self._driver, '_get_veth_state')
self.mock_object(self._driver, '_get_corresponding_veth',
mock.Mock(return_value='veth0'))
self.mock_object(self._driver, '_connect_to_network')
self.assertEqual(network_info['server_id'],
self._driver._setup_server(network_info)['id'])
self._driver.container.start_container.assert_called_once_with(
server_id)
self._driver._connect_to_network.assert_called_once_with(server_id,
network_info,
'veth0')

View File

@ -0,0 +1,295 @@
# 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.
"""Unit tests for the Protocol helper module."""
import functools
import mock
from manila.common import constants as const
from manila import exception
from manila.share.drivers.container import protocol_helper
from manila import test
from manila.tests.share.drivers.container.fakes import fake_share
class DockerCIFSHelperTestCase(test.TestCase):
"""Tests ContainerShareDriver"""
def setUp(self):
super(DockerCIFSHelperTestCase, self).setUp()
self._helper = mock.Mock()
self.fake_conf = mock.Mock()
self.fake_conf.container_cifs_guest_ok = "yes"
self.DockerCIFSHelper = protocol_helper.DockerCIFSHelper(
self._helper, share=fake_share(), config=self.fake_conf)
def fake_exec_sync(self, *args, **kwargs):
kwargs["execute_arguments"].append(args)
try:
ret_val = kwargs["ret_val"]
except KeyError:
ret_val = None
return [ret_val]
def test_create_share_guest_ok(self):
expected_arguments = [
("fakeserver", ["net", "conf", "addshare", "fakeshareid",
"/shares/fakeshareid", "writeable=y", "guest_ok=y"]),
("fakeserver", ["net", "conf", "setparm", "fakeshareid",
"browseable", "yes"]),
("fakeserver", ["net", "conf", "setparm", "fakeshareid",
"hosts allow", "127.0.0.1"]),
("fakeserver", ["net", "conf", "setparm", "fakeshareid",
"read only", "no"]),
("fakeserver", ["net", "conf", "setparm", "fakeshareid",
"hosts deny", "0.0.0.0/0"]),
("fakeserver", ["net", "conf", "setparm", "fakeshareid",
"create mask", "0755"])]
actual_arguments = []
self._helper.execute = functools.partial(
self.fake_exec_sync, execute_arguments=actual_arguments,
ret_val=" fake 192.0.2.2/24 more fake \n" * 20)
self.DockerCIFSHelper.share = fake_share()
self.DockerCIFSHelper.create_share("fakeserver")
self.assertEqual(expected_arguments.sort(), actual_arguments.sort())
def test_create_share_guest_not_ok(self):
self.DockerCIFSHelper.conf = mock.Mock()
self.DockerCIFSHelper.conf.container_cifs_guest_ok = False
expected_arguments = [
("fakeserver", ["net", "conf", "addshare", "fakeshareid",
"/shares/fakeshareid", "writeable=y", "guest_ok=n"]),
("fakeserver", ["net", "conf", "setparm", "fakeshareid",
"browseable", "yes"]),
("fakeserver", ["net", "conf", "setparm", "fakeshareid",
"hosts allow", "192.0.2.2"]),
("fakeserver", ["net", "conf", "setparm", "fakeshareid",
"read only", "no"]),
("fakeserver", ["net", "conf", "setparm", "fakeshareid",
"hosts deny", "0.0.0.0/0"]),
("fakeserver", ["net", "conf", "setparm", "fakeshareid",
"create mask", "0755"])]
actual_arguments = []
self._helper.execute = functools.partial(
self.fake_exec_sync, execute_arguments=actual_arguments,
ret_val=" fake 192.0.2.2/24 more fake \n" * 20)
self.DockerCIFSHelper.share = fake_share()
self.DockerCIFSHelper.create_share("fakeserver")
self.assertEqual(expected_arguments.sort(), actual_arguments.sort())
def test_delete_share(self):
self.DockerCIFSHelper.share = fake_share()
self.DockerCIFSHelper.delete_share("fakeserver")
self.DockerCIFSHelper.container.execute.assert_called_with(
"fakeserver",
["net", "conf", "delshare", "fakeshareid"])
def test__get_access_group_ro(self):
result = self.DockerCIFSHelper._get_access_group(const.ACCESS_LEVEL_RO)
self.assertEqual("read list", result)
def test__get_access_group_rw(self):
result = self.DockerCIFSHelper._get_access_group(const.ACCESS_LEVEL_RW)
self.assertEqual("valid users", result)
def test__get_access_group_other(self):
self.assertRaises(exception.InvalidShareAccessLevel,
self.DockerCIFSHelper._get_access_group,
"fake_level")
def test__get_existing_users(self):
self.DockerCIFSHelper.container.execute = mock.Mock(
return_value=("fake_user", ""))
result = self.DockerCIFSHelper._get_existing_users("fake_server_id",
"fake_share",
"fake_access")
self.assertEqual("fake_user", result)
self.DockerCIFSHelper.container.execute.assert_called_once_with(
"fake_server_id",
["net", "conf", "getparm", "fake_share", "fake_access"])
def test__set_users(self):
self.DockerCIFSHelper.container.execute = mock.Mock()
self.DockerCIFSHelper._set_users("fake_server_id", "fake_share",
"fake_access", "fake_user")
self.DockerCIFSHelper.container.execute.assert_called_once_with(
"fake_server_id",
["net", "conf", "setparm", "fake_share", "fake_access",
"fake_user"])
def test__allow_access_ok(self):
self.DockerCIFSHelper._get_access_group = mock.Mock(
return_value="valid users")
self.DockerCIFSHelper._get_existing_users = mock.Mock(
return_value="fake_user")
self.DockerCIFSHelper._set_users = mock.Mock()
self.DockerCIFSHelper._allow_access("fake_share", "fake_server_id",
"fake_user2", "rw")
self.DockerCIFSHelper._get_access_group.assert_called_once_with("rw")
self.DockerCIFSHelper._get_existing_users.assert_called_once_with(
"fake_server_id", "fake_share", "valid users")
self.DockerCIFSHelper._set_users.assert_called_once_with(
"fake_server_id", "fake_share", "valid users",
"fake_user fake_user2")
def test__allow_access_not_ok(self):
self.DockerCIFSHelper._get_access_group = mock.Mock(
return_value="valid users")
self.DockerCIFSHelper._get_existing_users = mock.Mock()
self.DockerCIFSHelper._get_existing_users.side_effect = TypeError
self.DockerCIFSHelper._set_users = mock.Mock()
self.DockerCIFSHelper._allow_access("fake_share", "fake_server_id",
"fake_user2", "rw")
self.DockerCIFSHelper._get_access_group.assert_called_once_with("rw")
self.DockerCIFSHelper._get_existing_users.assert_called_once_with(
"fake_server_id", "fake_share", "valid users")
self.DockerCIFSHelper._set_users.assert_called_once_with(
"fake_server_id", "fake_share", "valid users",
"fake_user2")
def test__deny_access_ok(self):
self.DockerCIFSHelper._get_access_group = mock.Mock(
return_value="valid users")
self.DockerCIFSHelper._get_existing_users = mock.Mock(
return_value="fake_user fake_user2")
self.DockerCIFSHelper._set_users = mock.Mock()
self.DockerCIFSHelper._deny_access("fake_share", "fake_server_id",
"fake_user2", "rw")
self.DockerCIFSHelper._get_access_group.assert_called_once_with("rw")
self.DockerCIFSHelper._get_existing_users.assert_called_once_with(
"fake_server_id", "fake_share", "valid users")
self.DockerCIFSHelper._set_users.assert_called_once_with(
"fake_server_id", "fake_share", "valid users",
"fake_user")
def test__deny_access_ok_so_many_users(self):
self.DockerCIFSHelper._get_access_group = mock.Mock(
return_value="valid users")
self.DockerCIFSHelper._get_existing_users = mock.Mock(
return_value="joost jaap huub dirk")
self.DockerCIFSHelper._set_users = mock.Mock()
# Sorry, Jaap.
self.DockerCIFSHelper._deny_access("fake_share", "fake_server_id",
"jaap", "rw")
self.DockerCIFSHelper._get_access_group.assert_called_once_with("rw")
self.DockerCIFSHelper._get_existing_users.assert_called_once_with(
"fake_server_id", "fake_share", "valid users")
self.DockerCIFSHelper._set_users.assert_called_once_with(
"fake_server_id", "fake_share", "valid users",
"dirk huub joost")
def test__deny_access_not_ok(self):
self.DockerCIFSHelper._get_access_group = mock.Mock(
return_value="valid users")
self.DockerCIFSHelper._get_existing_users = mock.Mock()
self.DockerCIFSHelper._get_existing_users.side_effect = TypeError
self.DockerCIFSHelper._set_users = mock.Mock()
protocol_helper.LOG.warning = mock.Mock()
self.DockerCIFSHelper._deny_access("fake_share", "fake_server_id",
"fake_user2", "rw")
self.DockerCIFSHelper._get_access_group.assert_called_once_with("rw")
self.DockerCIFSHelper._get_existing_users.assert_called_once_with(
"fake_server_id", "fake_share", "valid users")
self.assertFalse(self.DockerCIFSHelper._set_users.called)
self.assertTrue(protocol_helper.LOG.warning.called)
def test_update_access_access_rules_wrong_type(self):
allow_rules = [{
"access_to": "192.0.2.2",
"access_level": "ro",
"access_type": "fake"
}]
self.mock_object(self.DockerCIFSHelper, "_allow_access")
self.assertRaises(exception.InvalidShareAccess,
self.DockerCIFSHelper.update_access,
"fakeserver", allow_rules, [], [])
def test_update_access_access_rules_ok(self):
access_rules = [{
"access_to": "fakeuser",
"access_level": "ro",
"access_type": "user"
}]
self.mock_object(self.DockerCIFSHelper, "_allow_access")
self.DockerCIFSHelper.container.execute = mock.Mock()
self.DockerCIFSHelper.update_access("fakeserver",
access_rules, [], [])
self.DockerCIFSHelper._allow_access.assert_called_once_with(
"fakeshareid",
"fakeserver",
"fakeuser",
"ro")
self.DockerCIFSHelper.container.execute.assert_called_once_with(
"fakeserver",
["net", "conf", "setparm", "fakeshareid", "valid users", ""])
def test_update_access_add_rules(self):
add_rules = [{
"access_to": "fakeuser",
"access_level": "ro",
"access_type": "user"
}]
self.mock_object(self.DockerCIFSHelper, "_allow_access")
self.DockerCIFSHelper.update_access("fakeserver", [],
add_rules, [])
self.DockerCIFSHelper._allow_access.assert_called_once_with(
"fakeshareid",
"fakeserver",
"fakeuser",
"ro")
def test_update_access_delete_rules(self):
delete_rules = [{
"access_to": "fakeuser",
"access_level": "ro",
"access_type": "user"
}]
self.mock_object(self.DockerCIFSHelper, "_deny_access")
self.DockerCIFSHelper.update_access("fakeserver", [],
[], delete_rules)
self.DockerCIFSHelper._deny_access.assert_called_once_with(
"fakeshareid",
"fakeserver",
"fakeuser",
"ro")

View File

@ -0,0 +1,121 @@
# 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.
"""Unit tests for the Storage helper module."""
import functools
import mock
from manila import exception
from manila.share import configuration
from manila.share.drivers.container import storage_helper
from manila import test
from manila.tests.share.drivers.container.fakes import fake_share
class LVMHelperTestCase(test.TestCase):
"""Tests ContainerShareDriver"""
def setUp(self):
super(LVMHelperTestCase, self).setUp()
self.share = fake_share()
self.fake_conf = configuration.Configuration(None)
self.LVMHelper = storage_helper.LVMHelper(configuration=self.fake_conf)
def fake_exec_sync(self, *args, **kwargs):
kwargs['execute_arguments'].append(args)
try:
ret_val = kwargs['ret_val']
except KeyError:
ret_val = None
return ret_val
def test_lvmhelper_setup_explodes_in_gore_on_no_config_supplied(self):
self.assertRaises(exception.ManilaException,
storage_helper.LVMHelper,
None)
def test_get_share_server_pools(self):
ret_vgs = "VSize 100g size\nVFree 100g whatever"
expected_result = [{'reserved_percentage': 0,
'pool_name': 'manila_docker_volumes',
'total_capacity_gb': 100.0,
'free_capacity_gb': 100.0}]
self.mock_object(self.LVMHelper, "_execute",
mock.Mock(return_value=(ret_vgs, 0)))
result = self.LVMHelper.get_share_server_pools()
self.assertEqual(expected_result, result)
def test__get_lv_device(self):
self.assertEqual("/dev/manila_docker_volumes/fakeshareid",
self.LVMHelper._get_lv_device(self.share))
def test__get_lv_folder(self):
self.assertEqual("/tmp/shares/fakeshareid",
self.LVMHelper._get_lv_folder(self.share))
def test_provide_storage(self):
actual_arguments = []
expected_arguments = [
('lvcreate', '-p', 'rw', '-L', '1G', '-n', 'fakeshareid',
'manila_docker_volumes'),
('mkfs.ext4', '/dev/manila_docker_volumes/fakeshareid'),
('mount', '/dev/manila_docker_volumes/fakeshareid',
'/tmp/shares/fakeshareid'),
('chmod', '-R', '750', '/tmp/shares/fakeshareid'),
('chown', 'nobody:nogroup', '/tmp/shares/fakeshareid')
]
self.LVMHelper._execute = functools.partial(
self.fake_exec_sync, execute_arguments=actual_arguments,
ret_val='')
self.LVMHelper.provide_storage(self.share)
self.assertEqual(expected_arguments, actual_arguments)
def test_remove_storage(self):
actual_arguments = []
expected_arguments = [
('umount', '/dev/manila_docker_volumes/fakeshareid'),
('lvremove', '-f', '--autobackup', 'n',
'/dev/manila_docker_volumes/fakeshareid')
]
self.LVMHelper._execute = functools.partial(
self.fake_exec_sync, execute_arguments=actual_arguments,
ret_val='')
self.LVMHelper.remove_storage(self.share)
self.assertEqual(expected_arguments, actual_arguments)
def test_extend_share(self):
actual_arguments = []
expected_arguments = [
('umount', '/tmp/shares/fakeshareid'),
('lvextend', '-L', 'shareG', '-n',
'/dev/manila_docker_volumes/fakeshareid'),
('e2fsck', '-f', '-y', '/dev/manila_docker_volumes/fakeshareid'),
('resize2fs', '/dev/manila_docker_volumes/fakeshareid'),
('mount', '/dev/manila_docker_volumes/fakeshareid',
'/tmp/shares/fakeshareid')
]
self.LVMHelper._execute = functools.partial(
self.fake_exec_sync, execute_arguments=actual_arguments,
ret_val='')
self.LVMHelper.extend_share(self.share, 'share', 3)
self.assertEqual(expected_arguments, actual_arguments)

View File

@ -0,0 +1,14 @@
---
prelude: >
A new Container driver is added. It uses docker container as
a share server.
features:
- The Container driver allows using a docker container as a share server.
This allows for very fast share server startup.
- The Container driver supports CIFS protocol.
issues:
- |
The Container driver has the following known issues:
* Only basic driver operations are supported: create/delete share,
update access and extend share.