# Copyright 2014 Cloudbase Solutions Srl
# 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 abc
import functools
import os
import tempfile

from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import importutils
import six

from nova.i18n import _LE, _LW
from nova import utils

LOG = logging.getLogger(__name__)

libvirt_opts = [
    cfg.StrOpt('remote_filesystem_transport',
               default='ssh',
               choices=('ssh', 'rsync'),
               help='Use ssh or rsync transport for creating, copying, '
                    'removing files on the remote host.'),
    ]

CONF = cfg.CONF
CONF.register_opts(libvirt_opts, 'libvirt')


def mount_share(mount_path, export_path,
                export_type, options=None):
    """Mount a remote export to mount_path.

    :param mount_path: place where the remote export will be mounted
    :param export_path: path of the export to be mounted
    :export_type: remote export type (e.g. cifs, nfs, etc.)
    :options: A list containing mount options
    """
    utils.execute('mkdir', '-p', mount_path)

    mount_cmd = ['mount', '-t', export_type]
    if options is not None:
        mount_cmd.extend(options)
    mount_cmd.extend([export_path, mount_path])

    try:
        utils.execute(*mount_cmd, run_as_root=True)
    except processutils.ProcessExecutionError as exc:
        if 'Device or resource busy' in six.text_type(exc):
            LOG.warn(_LW("%s is already mounted"), export_path)
        else:
            raise


def unmount_share(mount_path, export_path):
    """Unmount a remote share.

    :param mount_path: remote export mount point
    :param export_path: path of the remote export to be unmounted
    """
    try:
        utils.execute('umount', mount_path, run_as_root=True,
                      attempts=3, delay_on_retry=True)
    except processutils.ProcessExecutionError as exc:
        if 'target is busy' in six.text_type(exc):
            LOG.debug("The share %s is still in use.", export_path)
        else:
            LOG.exception(_LE("Couldn't unmount the share %s"),
                          export_path)


class RemoteFilesystem(object):
    """Represents actions that can be taken on a remote host's filesystem."""

    def __init__(self):
        transport = CONF.libvirt.remote_filesystem_transport
        cls_name = '.'.join([__name__, transport.capitalize()])
        cls_name += 'Driver'
        self.driver = importutils.import_object(cls_name)

    def create_file(self, host, dst_path, on_execute=None,
                    on_completion=None):
        LOG.debug("Creating file %s on remote host %s", dst_path, host)
        self.driver.create_file(host, dst_path, on_execute=on_execute,
                                on_completion=on_completion)

    def remove_file(self, host, dst_path, on_execute=None,
                    on_completion=None):
        LOG.debug("Removing file %s on remote host %s", dst_path, host)
        self.driver.remove_file(host, dst_path, on_execute=on_execute,
                                on_completion=on_completion)

    def create_dir(self, host, dst_path, on_execute=None,
                    on_completion=None):
        LOG.debug("Creating directory %s on remote host %s", dst_path, host)
        self.driver.create_dir(host, dst_path, on_execute=on_execute,
                               on_completion=on_completion)

    def remove_dir(self, host, dst_path, on_execute=None,
                    on_completion=None):
        LOG.debug("Removing directory %s on remote host %s", dst_path, host)
        self.driver.remove_dir(host, dst_path, on_execute=on_execute,
                               on_completion=on_completion)

    def copy_file(self, src, dst, on_execute=None,
                    on_completion=None, compression=True):
        LOG.debug("Copying file %s to %s", src, dst)
        self.driver.copy_file(src, dst, on_execute=on_execute,
                              on_completion=on_completion,
                              compression=compression)


@six.add_metaclass(abc.ABCMeta)
class RemoteFilesystemDriver(object):
    @abc.abstractmethod
    def create_file(self, host, dst_path, on_execute, on_completion):
        """Create file on the remote system.

        :param host: Remote host
        :param dst_path: Destination path
        :param on_execute: Callback method to store pid of process in cache
        :param on_completion: Callback method to remove pid of process from
                              cache
        """

    @abc.abstractmethod
    def remove_file(self, host, dst_path, on_execute, on_completion):
        """Removes a file on a remote host.

        :param host: Remote host
        :param dst_path: Destination path
        :param on_execute: Callback method to store pid of process in cache
        :param on_completion: Callback method to remove pid of process from
                              cache
        """

    @abc.abstractmethod
    def create_dir(self, host, dst_path, on_execute, on_completion):
        """Create directory on the remote system.

        :param host: Remote host
        :param dst_path: Destination path
        :param on_execute: Callback method to store pid of process in cache
        :param on_completion: Callback method to remove pid of process from
                              cache
        """

    @abc.abstractmethod
    def remove_dir(self, host, dst_path, on_execute, on_completion):
        """Removes a directory on a remote host.

        :param host: Remote host
        :param dst_path: Destination path
        :param on_execute: Callback method to store pid of process in cache
        :param on_completion: Callback method to remove pid of process from
                              cache
        """

    @abc.abstractmethod
    def copy_file(self, src, dst, on_execute, on_completion):
        """Copy file to/from remote host.

        Remote address must be specified in format:
            REM_HOST_IP_ADDRESS:REM_HOST_PATH
        For example:
            192.168.1.10:/home/file

        :param src: Source address
        :param dst: Destination path
        :param on_execute: Callback method to store pid of process in cache
        :param on_completion: Callback method to remove pid of process from
        """


class SshDriver(RemoteFilesystemDriver):

    def create_file(self, host, dst_path, on_execute, on_completion):
        utils.ssh_execute(host, 'touch', dst_path,
                          on_execute=on_execute, on_completion=on_completion)

    def remove_file(self, host, dst, on_execute, on_completion):
        utils.ssh_execute(host, 'rm', dst,
                          on_execute=on_execute, on_completion=on_completion)

    def create_dir(self, host, dst_path, on_execute, on_completion):
        utils.ssh_execute(host, 'mkdir', '-p', dst_path,
                          on_execute=on_execute, on_completion=on_completion)

    def remove_dir(self, host, dst, on_execute, on_completion):
        utils.ssh_execute(host, 'rm', '-rf', dst,
                          on_execute=on_execute, on_completion=on_completion)

    def copy_file(self, src, dst, on_execute, on_completion, compression):
        utils.execute('scp', src, dst,
                      on_execute=on_execute, on_completion=on_completion)


def create_tmp_dir(function):
    """Creates temporary directory for rsync purposes.
    Removes created directory in the end.
    """

    @functools.wraps(function)
    def decorated_function(*args, **kwargs):
        # Create directory
        tmp_dir_path = tempfile.mkdtemp()
        kwargs['tmp_dir_path'] = tmp_dir_path

        try:
            return function(*args, **kwargs)
        finally:
            # Remove directory
            utils.execute('rm', '-rf', tmp_dir_path)

    return decorated_function


class RsyncDriver(RemoteFilesystemDriver):

    @create_tmp_dir
    def create_file(self, host, dst_path, on_execute, on_completion, **kwargs):
        dir_path = os.path.dirname(os.path.normpath(dst_path))

        # Create target dir inside temporary directory
        local_tmp_dir = os.path.join(kwargs['tmp_dir_path'],
                                     dir_path.strip(os.path.sep))
        utils.execute('mkdir', '-p', local_tmp_dir,
                      on_execute=on_execute, on_completion=on_completion)

        # Create file in directory
        file_name = os.path.basename(os.path.normpath(dst_path))
        local_tmp_file = os.path.join(local_tmp_dir, file_name)
        utils.execute('touch', local_tmp_file,
                      on_execute=on_execute, on_completion=on_completion)
        RsyncDriver._synchronize_object(kwargs['tmp_dir_path'],
                                        host, dst_path,
                                        on_execute=on_execute,
                                        on_completion=on_completion)

    @create_tmp_dir
    def remove_file(self, host, dst, on_execute, on_completion, **kwargs):
        # Delete file
        RsyncDriver._remove_object(kwargs['tmp_dir_path'], host, dst,
                                   on_execute=on_execute,
                                   on_completion=on_completion)

    @create_tmp_dir
    def create_dir(self, host, dst_path, on_execute, on_completion, **kwargs):
        dir_path = os.path.normpath(dst_path)

        # Create target dir inside temporary directory
        local_tmp_dir = os.path.join(kwargs['tmp_dir_path'],
                                 dir_path.strip(os.path.sep))
        utils.execute('mkdir', '-p', local_tmp_dir,
                      on_execute=on_execute, on_completion=on_completion)
        RsyncDriver._synchronize_object(kwargs['tmp_dir_path'],
                                        host, dst_path,
                                        on_execute=on_execute,
                                        on_completion=on_completion)

    @create_tmp_dir
    def remove_dir(self, host, dst, on_execute, on_completion, **kwargs):
        # Remove remote directory's content
        utils.execute('rsync', '--archive', '--delete-excluded',
                      kwargs['tmp_dir_path'] + os.path.sep,
                      utils.format_remote_path(host, dst),
                      on_execute=on_execute, on_completion=on_completion)

        # Delete empty directory
        RsyncDriver._remove_object(kwargs['tmp_dir_path'], host, dst,
                                   on_execute=on_execute,
                                   on_completion=on_completion)

    @staticmethod
    def _remove_object(src, host, dst, on_execute, on_completion):
        """Removes a file or empty directory on a remote host.

        :param src: Empty directory used for rsync purposes
        :param host: Remote host
        :param dst: Destination path
        :param on_execute: Callback method to store pid of process in cache
        :param on_completion: Callback method to remove pid of process from
                              cache
        """
        utils.execute('rsync', '--archive', '--delete',
                      '--include', os.path.basename(os.path.normpath(dst)),
                      '--exclude', '*',
                      os.path.normpath(src) + os.path.sep,
                      utils.format_remote_path(host,
                                 os.path.dirname(os.path.normpath(dst))),
                      on_execute=on_execute, on_completion=on_completion)

    @staticmethod
    def _synchronize_object(src, host, dst, on_execute, on_completion):
        """Creates a file or empty directory on a remote host.

        :param src: Empty directory used for rsync purposes
        :param host: Remote host
        :param dst: Destination path
        :param on_execute: Callback method to store pid of process in cache
        :param on_completion: Callback method to remove pid of process from
                              cache
        """

        # For creating path on the remote host rsync --relative path must
        # be used. With a modern rsync on the sending side (beginning with
        # 2.6.7), you can insert a dot and a slash into the source path,
        # like this:
        #   rsync -avR /foo/./bar/baz.c remote:/tmp/
        # That would create /tmp/bar/baz.c on the remote machine.
        # (Note that the dot must be followed by a slash, so "/foo/."
        # would not be abbreviated.)
        relative_tmp_file_path = os.path.join(
            src, './',
            os.path.normpath(dst).strip(os.path.sep))

        # Do relative rsync local directory with remote root directory
        utils.execute('rsync', '--archive', '--relative', '--no-implied-dirs',
                      relative_tmp_file_path,
                      utils.format_remote_path(host, os.path.sep),
                      on_execute=on_execute, on_completion=on_completion)

    def copy_file(self, src, dst, on_execute, on_completion, compression):
        args = ['rsync', '--sparse', src, dst]
        if compression:
            args.append('--compress')
        utils.execute(*args,
                      on_execute=on_execute, on_completion=on_completion)
