# 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
import uuid

from oslo_log import log
from oslo_utils import excutils

from manila import exception
from manila.i18n import _
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.
        #
        # (aovchinnikov): To actually make docker container mount a
        # logical volume created after container start-up to some location
        # inside it, we must share entire /dev with it. While seemingly
        # dangerous it is not and moreover this is apparently the only sane
        # way to do it. The reason is when a logical volume gets created
        # several new things appear in /dev: a new /dev/dm-X and a symlink
        # in /dev/volume_group_name pointing to /dev/dm-X. But to be able
        # to interact with /dev/dm-X, it must be already present inside
        # the container's /dev i.e. it must have been -v shared during
        # container start-up. So we should either precreate an unknown
        # number of /dev/dm-Xs (one per LV), share them all and hope
        # for the best or share the entire /dev and hope for the best.
        #
        # The risk of allowing a container having access to entire host's
        # /dev is not as big as it seems: as long as actual share providers
        # are invulnerable this does not pose any extra risks. If, however,
        # share providers contain vulnerabilities then the driver does not
        # provide any more possibilities for an exploitation than other
        # first-party drivers.

        path = "{0}:/shares".format(
            self.configuration.container_volume_mount_path)
        cmd = ["docker", "run", "-d", "-i", "-t", "--privileged",
               "-v", "/dev:/dev", "--name=%s" % name,
               "-v", path, image_name]
        try:
            result = self._inner_execute(cmd)
        except (exception.ProcessExecutionError, OSError):
            raise exception.ShareBackendException(
                msg="Container %s has failed to start." % name)
        LOG.info("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)
        try:
            self._inner_execute(["docker", "stop", name])
        except (exception.ProcessExecutionError, OSError):
            raise exception.ShareBackendException(
                msg="Container %s has failed to stop properly." % name)
        LOG.info("Container %s is successfully stopped.", name)

    def execute(self, name=None, cmd=None, ignore_errors=False):
        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, ignore_errors=ignore_errors)
        return result

    def _inner_execute(self, cmd, ignore_errors=False):
        LOG.debug("Executing command: %s.", " ".join(cmd))
        try:
            result = self._execute(*cmd, run_as_root=True)
        except (exception.ProcessExecutionError, OSError) as e:
            with excutils.save_and_reraise_exception(
                    reraise=not ignore_errors):
                LOG.warning("Failed to run command %(cmd)s due to "
                            "%(reason)s.", {'cmd': cmd, 'reason': e})
        else:
            LOG.debug("Execution result: %s.", result)
            return result

    def fetch_container_address(self, name, address_family="inet6"):
        result = self.execute(
            name,
            ["ip", "-oneline",
             "-family", address_family,
             "address", "show", "scope", "global", "dev", "eth0"],
        )
        address_w_prefix = result[0].split()[3]
        address = address_w_prefix.split('/')[0]
        return address

    def rename_container(self, name, new_name):
        veth_name = self.find_container_veth(name)
        if not veth_name:
            raise exception.ManilaException(
                _("Could not find OVS information related to "
                  "container %s.") % name)

        try:
            self._inner_execute(["docker", "rename", name, new_name])
        except (exception.ProcessExecutionError, OSError):
            raise exception.ShareBackendException(
                msg="Could not rename container %s." % name)

        cmd = ["ovs-vsctl", "set", "interface", veth_name,
               "external-ids:manila-container=%s" % new_name]
        try:
            self._inner_execute(cmd)
        except (exception.ProcessExecutionError, OSError):
            try:
                self._inner_execute(["docker", "rename", new_name, name])
            except (exception.ProcessExecutionError, OSError):
                msg = _("Could not rename back container %s.") % name
                LOG.exception(msg)
            raise exception.ShareBackendException(
                msg="Could not update OVS information %s." % name)

        LOG.info("Container %s has been successfully renamed.", name)

    def find_container_veth(self, name):
        interfaces = self._execute("ovs-vsctl", "list", "interface",
                                   run_as_root=True)[0]
        veths = set(re.findall("veth[0-9a-zA-Z]{7}", interfaces))
        manila_re = "manila-container=\"?.{%s}\"?" % len(name)
        for veth in veths:
            try:
                iface_data = self._execute("ovs-vsctl", "list", "interface",
                                           veth, run_as_root=True)[0]
            except (exception.ProcessExecutionError, OSError) as e:
                LOG.debug("Error listing interface %(veth)s. "
                          "Reason: %(reason)s", {'veth': veth,
                                                 'reason': e})
                continue

            container_id = re.findall(manila_re, iface_data)
            if container_id == []:
                continue
            elif container_id[0].split("manila-container=")[-1].split(
                    "manila_")[-1].strip('"') == name.split("manila_")[-1]:
                return veth

    def container_exists(self, name):

        result = self._execute("docker", "ps", "--no-trunc",
                               "--format='{{.Names}}'", run_as_root=True)[0]
        for line in result.split('\n'):
            if name == line.strip("'"):
                return True
        return False
