diff --git a/contrib/ci/post_test_hook.sh b/contrib/ci/post_test_hook.sh index 88841a08bd..a800ac2a5e 100755 --- a/contrib/ci/post_test_hook.sh +++ b/contrib/ci/post_test_hook.sh @@ -208,6 +208,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 diff --git a/contrib/ci/pre_test_hook.sh b/contrib/ci/pre_test_hook.sh index f30406b9e7..8f5a911af1 100755 --- a/contrib/ci/pre_test_hook.sh +++ b/contrib/ci/pre_test_hook.sh @@ -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 diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 564dd27efa..6fca7ed592 100755 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -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 @@ -599,6 +630,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 @@ -696,6 +741,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 diff --git a/devstack/settings b/devstack/settings index 6f60b07b1a..a534fc6dea 100644 --- a/devstack/settings +++ b/devstack/settings @@ -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 diff --git a/doc/source/devref/container_driver.rst b/doc/source/devref/container_driver.rst new file mode 100644 index 0000000000..65faec273c --- /dev/null +++ b/doc/source/devref/container_driver.rst @@ -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 + 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= + SHARE_DRIVER=manila.share.drivers.container.driver.ContainerShareDriver + SHARE_BACKING_FILE_SIZE= + MANILA_DEFAULT_SHARE_TYPE_EXTRA_SPECS='snapshot_support=false' + +where is change reference, which could be copied from gerrit web-interface, + 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 ' command. Resulting tar-archive of the +image could be uploaded to docker via + +.. code-block:: console + + sudo docker load --input + +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 = + +where 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. diff --git a/doc/source/devref/index.rst b/doc/source/devref/index.rst index afc90d5477..87dcc7341f 100644 --- a/doc/source/devref/index.rst +++ b/doc/source/devref/index.rst @@ -104,6 +104,7 @@ Share backends .. toctree:: :maxdepth: 3 + container_driver zfs_on_linux_driver netapp_cluster_mode_driver emc_isilon_driver diff --git a/doc/source/devref/share_back_ends_feature_support_mapping.rst b/doc/source/devref/share_back_ends_feature_support_mapping.rst index 1ae6d0011f..0fb52d6760 100644 --- a/doc/source/devref/share_back_ends_feature_support_mapping.rst +++ b/doc/source/devref/share_back_ends_feature_support_mapping.rst @@ -35,6 +35,8 @@ Mapping of share drivers and share features support +========================================+=======================+=======================+==============+==============+========================+============================+==========================+ | ZFSonLinux | M | N | M | M | M | M | N | +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+ +| 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 | @@ -82,6 +84,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) | \- | \- | @@ -127,6 +131,8 @@ Mapping of share drivers and security services support +========================================+==================+=================+==================+ | ZFSonLinux | \- | \- | \- | +----------------------------------------+------------------+-----------------+------------------+ +| Container | \- | \- | \- | ++----------------------------------------+------------------+-----------------+------------------+ | Generic (Cinder as back-end) | \- | \- | \- | +----------------------------------------+------------------+-----------------+------------------+ | NetApp Clustered Data ONTAP | J | J | J | @@ -172,6 +178,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 | \- | diff --git a/etc/manila/rootwrap.d/share.filters b/etc/manila/rootwrap.d/share.filters index cf26c0b1ac..907ca8d605 100644 --- a/etc/manila/rootwrap.d/share.filters +++ b/etc/manila/rootwrap.d/share.filters @@ -149,3 +149,12 @@ ls: CommandFilter, ls, root # manila/data/utils.py: 'touch', '--reference=%s', '%s' touch: CommandFilter, touch, root + +# manila/share/drivers/container/container.py: docker +docker: CommandFilter, docker, root + +# manila/share/drivers/container/container.py: brctl +brctl: CommandFilter, brctl, root + +# manila/share/drivers/container/container.py: e2fsck +e2fsck: CommandFilter, e2fsck, root diff --git a/manila/network/neutron/neutron_network_plugin.py b/manila/network/neutron/neutron_network_plugin.py index b930b10f79..c31a664a7d 100644 --- a/manila/network/neutron/neutron_network_plugin.py +++ b/manila/network/neutron/neutron_network_plugin.py @@ -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'], diff --git a/manila/opts.py b/manila/opts.py index 870077005c..7455ae9b7f 100644 --- a/manila/opts.py +++ b/manila/opts.py @@ -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, diff --git a/manila/share/drivers/container/__init__.py b/manila/share/drivers/container/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/share/drivers/container/container_helper.py b/manila/share/drivers/container/container_helper.py new file mode 100644 index 0000000000..eba03aaa11 --- /dev/null +++ b/manila/share/drivers/container/container_helper.py @@ -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 diff --git a/manila/share/drivers/container/driver.py b/manila/share/drivers/container/driver.py new file mode 100644 index 0000000000..931cb08fac --- /dev/null +++ b/manila/share/drivers/container/driver.py @@ -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"]} diff --git a/manila/share/drivers/container/protocol_helper.py b/manila/share/drivers/container/protocol_helper.py new file mode 100644 index 0000000000..0a6e4225b6 --- /dev/null +++ b/manila/share/drivers/container/protocol_helper.py @@ -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) diff --git a/manila/share/drivers/container/storage_helper.py b/manila/share/drivers/container/storage_helper.py new file mode 100644 index 0000000000..2e9e658003 --- /dev/null +++ b/manila/share/drivers/container/storage_helper.py @@ -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) diff --git a/manila/tests/network/neutron/test_neutron_plugin.py b/manila/tests/network/neutron/test_neutron_plugin.py index ce16f40dce..eeb333b34d 100644 --- a/manila/tests/network/neutron/test_neutron_plugin.py +++ b/manila/tests/network/neutron/test_neutron_plugin.py @@ -132,7 +132,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) @@ -141,6 +141,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', @@ -169,11 +217,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), diff --git a/manila/tests/share/drivers/container/__init__.py b/manila/tests/share/drivers/container/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/share/drivers/container/fakes.py b/manila/tests/share/drivers/container/fakes.py new file mode 100644 index 0000000000..1c363534be --- /dev/null +++ b/manila/tests/share/drivers/container/fakes.py @@ -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) diff --git a/manila/tests/share/drivers/container/test_container_helper.py b/manila/tests/share/drivers/container/test_container_helper.py new file mode 100644 index 0000000000..e7f7f281b9 --- /dev/null +++ b/manila/tests/share/drivers/container/test_container_helper.py @@ -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) diff --git a/manila/tests/share/drivers/container/test_driver.py b/manila/tests/share/drivers/container/test_driver.py new file mode 100644 index 0000000000..fb91434c7c --- /dev/null +++ b/manila/tests/share/drivers/container/test_driver.py @@ -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') diff --git a/manila/tests/share/drivers/container/test_protocol_helper.py b/manila/tests/share/drivers/container/test_protocol_helper.py new file mode 100644 index 0000000000..3bab5df0f9 --- /dev/null +++ b/manila/tests/share/drivers/container/test_protocol_helper.py @@ -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") diff --git a/manila/tests/share/drivers/container/test_storage_helper.py b/manila/tests/share/drivers/container/test_storage_helper.py new file mode 100644 index 0000000000..f6cb6a64ec --- /dev/null +++ b/manila/tests/share/drivers/container/test_storage_helper.py @@ -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) diff --git a/releasenotes/notes/container-driver-5d972cc40e314663.yaml b/releasenotes/notes/container-driver-5d972cc40e314663.yaml new file mode 100644 index 0000000000..ff94d180b4 --- /dev/null +++ b/releasenotes/notes/container-driver-5d972cc40e314663.yaml @@ -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.