# Copyright (c) 2013 OpenStack Foundation
# 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.

"""Remote filesystem client utilities."""

import hashlib
import os
import re
import tempfile

from oslo_concurrency import processutils as putils
from oslo_log import log as logging
import six

from os_brick import exception
from os_brick.i18n import _, _LI

LOG = logging.getLogger(__name__)


class RemoteFsClient(object):

    def __init__(self, mount_type, root_helper,
                 execute=putils.execute, *args, **kwargs):

        mount_type_to_option_prefix = {
            'nfs': 'nfs',
            'cifs': 'smbfs',
            'glusterfs': 'glusterfs',
            'vzstorage': 'vzstorage',
            'scality': 'scality_sofs'
        }

        if mount_type not in mount_type_to_option_prefix:
            raise exception.ProtocolNotSupported(protocol=mount_type)

        self._mount_type = mount_type
        option_prefix = mount_type_to_option_prefix[mount_type]

        self._mount_base = kwargs.get(option_prefix + '_mount_point_base')
        if not self._mount_base:
            raise exception.InvalidParameterValue(
                err=_('%s_mount_point_base required') % option_prefix)

        self._mount_options = kwargs.get(option_prefix + '_mount_options')

        if mount_type == "nfs":
            self._check_nfs_options()

        self.root_helper = root_helper
        self.set_execute(execute)

    def set_execute(self, execute):
        self._execute = execute

    def _get_hash_str(self, base_str):
        """Return a string that represents hash of base_str
        (in a hex format).
        """
        if isinstance(base_str, six.text_type):
            base_str = base_str.encode('utf-8')
        return hashlib.md5(base_str).hexdigest()

    def get_mount_point(self, device_name):
        """Get Mount Point.

        :param device_name: example 172.18.194.100:/var/nfs
        """
        return os.path.join(self._mount_base,
                            self._get_hash_str(device_name))

    def _read_mounts(self):
        (out, _err) = self._execute('mount', check_exit_code=0)
        lines = out.split('\n')
        mounts = {}
        for line in lines:
            tokens = line.split()
            if 2 < len(tokens):
                device = tokens[0]
                mnt_point = tokens[2]
                mounts[mnt_point] = device
        return mounts

    def mount(self, share, flags=None):
        """Mount given share."""
        mount_path = self.get_mount_point(share)

        if mount_path in self._read_mounts():
            LOG.info(_LI('Already mounted: %s'), mount_path)
            return

        self._execute('mkdir', '-p', mount_path, check_exit_code=0)
        if self._mount_type == 'nfs':
            self._mount_nfs(share, mount_path, flags)
        elif self._mount_type == 'vzstorage':
            self._mount_vzstorage(share, mount_path, flags)
        else:
            self._do_mount(self._mount_type, share, mount_path,
                           self._mount_options, flags)

    def _do_mount(self, mount_type, share, mount_path, mount_options=None,
                  flags=None):
        """Mounts share based on the specified params."""
        mnt_cmd = ['mount', '-t', mount_type]
        if mount_options is not None:
            mnt_cmd.extend(['-o', mount_options])
        if flags is not None:
            mnt_cmd.extend(flags)
        mnt_cmd.extend([share, mount_path])

        self._execute(*mnt_cmd, root_helper=self.root_helper,
                      run_as_root=True, check_exit_code=0)

    def _mount_nfs(self, nfs_share, mount_path, flags=None):
        """Mount nfs share using present mount types."""
        mnt_errors = {}

        # This loop allows us to first try to mount with NFS 4.1 for pNFS
        # support but falls back to mount NFS 4 or NFS 3 if either the client
        # or server do not support it.
        for mnt_type in sorted(self._nfs_mount_type_opts.keys(), reverse=True):
            options = self._nfs_mount_type_opts[mnt_type]
            try:
                self._do_mount('nfs', nfs_share, mount_path, options, flags)
                LOG.debug('Mounted %(sh)s using %(mnt_type)s.',
                          {'sh': nfs_share, 'mnt_type': mnt_type})
                return
            except Exception as e:
                mnt_errors[mnt_type] = six.text_type(e)
                LOG.debug('Failed to do %s mount.', mnt_type)
        raise exception.BrickException(_("NFS mount failed for share %(sh)s. "
                                         "Error - %(error)s")
                                       % {'sh': nfs_share,
                                          'error': mnt_errors})

    def _vzstorage_write_mds_list(self, cluster_name, mdss):
        tmp_dir = tempfile.mkdtemp(prefix='vzstorage-')
        tmp_bs_path = os.path.join(tmp_dir, 'bs_list')
        with open(tmp_bs_path, 'w') as f:
            for mds in mdss:
                f.write(mds + "\n")

        conf_dir = os.path.join('/etc/pstorage/clusters', cluster_name)
        if os.path.exists(conf_dir):
            bs_path = os.path.join(conf_dir, 'bs_list')
            self._execute('cp', '-f', tmp_bs_path, bs_path,
                          root_helper=self.root_helper, run_as_root=True)
        else:
            self._execute('cp', '-rf', tmp_dir, conf_dir,
                          root_helper=self.root_helper, run_as_root=True)
        self._execute('chown', '-R', 'root:root', conf_dir,
                      root_helper=self.root_helper, run_as_root=True)

    def _mount_vzstorage(self, vz_share, mount_path, flags=None):
        m = re.search("(?:(\S+):\/)?([a-zA-Z0-9_-]+)(?::(\S+))?", vz_share)
        if not m:
            msg = (_("Invalid Virtuozzo Storage share specification: %r."
                     "Must be: [MDS1[,MDS2],...:/]<CLUSTER NAME>[:PASSWORD].")
                   % vz_share)
            raise exception.BrickException(msg)

        mdss = m.group(1)
        cluster_name = m.group(2)
        passwd = m.group(3)

        if mdss:
            mdss = mdss.split(',')
            self._vzstorage_write_mds_list(cluster_name, mdss)

        if passwd:
            self._execute('pstorage', '-c', cluster_name, 'auth-node', '-P',
                          process_input=passwd,
                          root_helper=self.root_helper, run_as_root=True)

        mnt_cmd = ['pstorage-mount', '-c', cluster_name]
        if flags:
            mnt_cmd.extend(flags)
        mnt_cmd.extend([mount_path])

        self._execute(*mnt_cmd, root_helper=self.root_helper,
                      run_as_root=True, check_exit_code=0)

    def _check_nfs_options(self):
        """Checks and prepares nfs mount type options."""
        self._nfs_mount_type_opts = {'nfs': self._mount_options}
        nfs_vers_opt_patterns = ['^nfsvers', '^vers', '^v[\d]']
        for opt in nfs_vers_opt_patterns:
            if self._option_exists(self._mount_options, opt):
                return

        # pNFS requires NFS 4.1. The mount.nfs4 utility does not automatically
        # negotiate 4.1 support, we have to ask for it by specifying two
        # options: vers=4 and minorversion=1.
        pnfs_opts = self._update_option(self._mount_options, 'vers', '4')
        pnfs_opts = self._update_option(pnfs_opts, 'minorversion', '1')
        self._nfs_mount_type_opts['pnfs'] = pnfs_opts

    def _option_exists(self, options, opt_pattern):
        """Checks if the option exists in nfs options and returns position."""
        options = [x.strip() for x in options.split(',')] if options else []
        pos = 0
        for opt in options:
            pos = pos + 1
            if re.match(opt_pattern, opt, flags=0):
                return pos
        return 0

    def _update_option(self, options, option, value=None):
        """Update option if exists else adds it and returns new options."""
        opts = [x.strip() for x in options.split(',')] if options else []
        pos = self._option_exists(options, option)
        if pos:
            opts.pop(pos - 1)
        opt = '%s=%s' % (option, value) if value else option
        opts.append(opt)
        return ",".join(opts) if len(opts) > 1 else opts[0]
