# Copyright (c) 2019 Infortrend Technology, 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 json
import re

from oslo_concurrency import processutils
from oslo_log import log
from oslo_utils import units

from manila.common import constants
from manila import exception
from manila.i18n import _
from manila.share import utils as share_utils
from manila import ssh_utils
from manila import utils as manila_utils


LOG = log.getLogger(__name__)


def _bi_to_gi(bi_size):
    return bi_size / units.Gi


class InfortrendNAS(object):

    _SSH_PORT = 22

    def __init__(self, nas_ip, username, password, ssh_key,
                 timeout, pool_dict, channel_dict):
        self.nas_ip = nas_ip
        self.port = self._SSH_PORT
        self.username = username
        self.password = password
        self.ssh_key = ssh_key
        self.ssh_timeout = timeout
        self.pool_dict = pool_dict
        self.channel_dict = channel_dict
        self.command = ""
        self.ssh = None
        self.sshpool = None
        self.location = 'a@0'

    def _execute(self, command_line):
        command_line.extend(['-z', self.location])
        commands = ' '.join(command_line)
        manila_utils.check_ssh_injection(commands)
        LOG.debug('Executing: %(command)s', {'command': commands})

        cli_out = self._ssh_execute(commands)

        return self._parser(cli_out)

    def _ssh_execute(self, commands):
        try:
            out, err = processutils.ssh_execute(
                self.ssh, commands,
                timeout=self.ssh_timeout, check_exit_code=True)
        except processutils.ProcessExecutionError as pe:
            rc = pe.exit_code
            out = pe.stdout
            out = out.replace('\n', '\\n')
            msg = _('Error on execute ssh command. '
                    'Exit code: %(rc)d, msg: %(out)s') % {
                        'rc': rc, 'out': out}
            raise exception.InfortrendNASException(err=msg)

        return out

    def _parser(self, content=None):
        LOG.debug('parsing data:\n%s', content)
        content = content.replace("\r", "")
        content = content.strip()
        json_string = content.replace("'", "\"")
        cli_data = json_string.splitlines()[2]
        if cli_data:
            try:
                data_dict = json.loads(cli_data)
            except Exception:
                msg = _('Failed to parse data: '
                        '%(cli_data)s to dictionary.') % {
                            'cli_data': cli_data}
                LOG.error(msg)
                raise exception.InfortrendNASException(err=msg)

            rc = int(data_dict['cliCode'][0]['Return'], 16)
            if rc == 0:
                result = data_dict['data']
            else:
                result = data_dict['cliCode'][0]['CLI']
        else:
            msg = _('No data is returned from NAS.')
            LOG.error(msg)
            raise exception.InfortrendNASException(err=msg)

        if rc != 0:
            msg = _('NASCLI error, returned: %(result)s.') % {
                'result': result}
            LOG.error(msg)
            raise exception.InfortrendCLIException(
                err=msg, rc=rc, out=result)

        return rc, result

    def do_setup(self):
        self._init_connect()
        self._ensure_service_on('nfs')
        self._ensure_service_on('cifs')

    def _init_connect(self):
        if not (self.sshpool and self.ssh):
            self.sshpool = ssh_utils.SSHPool(ip=self.nas_ip,
                                             port=self.port,
                                             conn_timeout=None,
                                             login=self.username,
                                             password=self.password,
                                             privatekey=self.ssh_key)
            self.ssh = self.sshpool.create()

        if not self.ssh.get_transport().is_active():
            self.sshpool = ssh_utils.SSHPool(ip=self.nas_ip,
                                             port=self.port,
                                             conn_timeout=None,
                                             login=self.username,
                                             password=self.password,
                                             privatekey=self.ssh_key)
            self.ssh = self.sshpool.create()

        LOG.debug('NAScmd [%s@%s] start!', self.username, self.nas_ip)

    def check_for_setup_error(self):
        self._check_pools_setup()
        self._check_channels_status()

    def _ensure_service_on(self, proto, slot='A'):
        command_line = ['service', 'status', proto]
        rc, service_status = self._execute(command_line)
        if not service_status[0][slot][proto.upper()]['enabled']:
            command_line = ['service', 'restart', proto]
            self._execute(command_line)

    def _check_channels_status(self):
        channel_list = list(self.channel_dict.keys())
        command_line = ['ifconfig', 'inet', 'show']
        rc, channels_status = self._execute(command_line)
        for channel in channels_status:
            if 'CH' in channel['datalink']:
                ch = channel['datalink'].strip('CH')
                if ch in self.channel_dict.keys():
                    self.channel_dict[ch] = channel['IP']
                    channel_list.remove(ch)
                    if channel['status'] == 'DOWN':
                        LOG.warning('Channel [%(ch)s] status '
                                    'is down, please check.', {
                                        'ch': ch})
        if len(channel_list) != 0:
            msg = _('Channel setting %(channel_list)s is invalid!') % {
                'channel_list': channel_list}
            LOG.error(msg)
            raise exception.InfortrendNASException(message=msg)

    def _check_pools_setup(self):
        pool_list = list(self.pool_dict.keys())
        command_line = ['folder', 'status']
        rc, pool_data = self._execute(command_line)
        for pool in pool_data:
            pool_name = self._extract_pool_name(pool)
            if pool_name in self.pool_dict.keys():
                pool_list.remove(pool_name)
                self.pool_dict[pool_name]['id'] = pool['volumeId']
                self.pool_dict[pool_name]['path'] = pool['directory'] + '/'
            if len(pool_list) == 0:
                break

        if len(pool_list) != 0:
            msg = _('Please create %(pool_list)s pool/s in advance!') % {
                'pool_list': pool_list}
            LOG.error(msg)
            raise exception.InfortrendNASException(message=msg)

    def _extract_pool_name(self, pool_info):
        return pool_info['directory'].split('/')[1]

    def _extract_lv_name(self, pool_info):
        return pool_info['path'].split('/')[2]

    def update_pools_stats(self):
        pools = []
        command_line = ['folder', 'status']
        rc, pools_data = self._execute(command_line)

        for pool_info in pools_data:
            pool_name = self._extract_pool_name(pool_info)

            if pool_name in self.pool_dict.keys():
                total_space = float(pool_info['size'])
                pool_quota_used = self._get_pool_quota_used(pool_name)
                available_space = total_space - pool_quota_used

                total_capacity_gb = round(_bi_to_gi(total_space), 2)
                free_capacity_gb = round(_bi_to_gi(available_space), 2)

                pool = {
                    'pool_name': pool_name,
                    'total_capacity_gb': total_capacity_gb,
                    'free_capacity_gb': free_capacity_gb,
                    'reserved_percentage': 0,
                    'qos': False,
                    'dedupe': False,
                    'compression': False,
                    'snapshot_support': False,
                    'thin_provisioning': False,
                    'thick_provisioning': True,
                    'replication_type': None,
                }
                pools.append(pool)

        return pools

    def _get_pool_quota_used(self, pool_name):
        pool_quota_used = 0.0
        pool_data = self._get_share_pool_data(pool_name)
        folder_name = self._extract_lv_name(pool_data)

        command_line = ['fquota', 'status', pool_data['id'],
                        folder_name, '-t', 'folder']
        rc, quota_status = self._execute(command_line)

        for share_quota in quota_status:
            pool_quota_used += int(share_quota['quota'])

        return pool_quota_used

    def _get_share_pool_data(self, pool_name):
        if not pool_name:
            msg = _("Pool is not available in the share host.")
            raise exception.InvalidHost(reason=msg)

        if pool_name in self.pool_dict.keys():
            return self.pool_dict[pool_name]
        else:
            msg = _('Pool [%(pool_name)s] not set in conf.') % {
                'pool_name': pool_name}
            LOG.error(msg)
            raise exception.InfortrendNASException(err=msg)

    def create_share(self, share, share_server=None):
        pool_name = share_utils.extract_host(share['host'], level='pool')
        pool_data = self._get_share_pool_data(pool_name)
        folder_name = self._extract_lv_name(pool_data)
        share_proto = share['share_proto'].lower()
        share_name = share['id'].replace('-', '')
        share_path = pool_data['path'] + share_name

        command_line = ['folder', 'options', pool_data['id'],
                        folder_name, '-c', share_name]
        self._execute(command_line)

        self._set_share_size(
            pool_data['id'], pool_name, share_name, share['size'])
        self._ensure_protocol_on(share_path, share_proto, share_name)

        LOG.info('Create Share [%(share)s] completed.', {
            'share': share['id']})

        return self._export_location(
            share_name, share_proto, pool_data['path'])

    def _export_location(self, share_name, share_proto, pool_path=None):
        location = []
        location_data = {
            'pool_path': pool_path,
            'share_name': share_name,
        }
        self._check_channels_status()
        for ch in sorted(self.channel_dict.keys()):
            ip = self.channel_dict[ch]
            if share_proto == 'nfs':
                location.append(
                    ip + ':%(pool_path)s%(share_name)s' % location_data)
            elif share_proto == 'cifs':
                location.append(
                    '\\\\' + ip + '\\%(share_name)s' % location_data)
            else:
                msg = _('Unsupported protocol: [%s].') % share_proto
                raise exception.InvalidInput(msg)

        return location

    def _set_share_size(self, pool_id, pool_name, share_name, share_size):
        pool_data = self._get_share_pool_data(pool_name)
        folder_name = self._extract_lv_name(pool_data)
        command_line = ['fquota', 'create', pool_id, folder_name,
                        share_name, str(share_size) + 'G', '-t', 'folder']
        self._execute(command_line)

        LOG.debug('Set Share [%(share_name)s] '
                  'Size [%(share_size)s G] completed.', {
                      'share_name': share_name,
                      'share_size': share_size})
        return

    def _get_share_size(self, pool_id, pool_name, share_name):
        share_size = None
        command_line = ['fquota', 'status', pool_id,
                        share_name, '-t', 'folder']
        rc, quota_status = self._execute(command_line)

        for share_quota in quota_status:
            if share_quota['name'] == share_name:
                share_size = round(_bi_to_gi(float(share_quota['quota'])), 2)
                break

        return share_size

    def delete_share(self, share, share_server=None):
        pool_name = share_utils.extract_host(share['host'], level='pool')
        pool_data = self._get_share_pool_data(pool_name)
        folder_name = self._extract_lv_name(pool_data)
        share_name = share['id'].replace('-', '')

        if self._check_share_exist(pool_name, share_name):
            command_line = ['folder', 'options', pool_data['id'],
                            folder_name, '-d', share_name]
            self._execute(command_line)
        else:
            LOG.warning('Share [%(share_name)s] is already deleted.', {
                'share_name': share_name})

        LOG.info('Delete Share [%(share)s] completed.', {
            'share': share['id']})

    def _check_share_exist(self, pool_name, share_name):
        path = self.pool_dict[pool_name]['path']
        command_line = ['pagelist', 'folder', path]
        rc, subfolders = self._execute(command_line)
        return any(subfolder['name'] == share_name for subfolder in subfolders)

    def update_access(self, share, access_rules, add_rules,
                      delete_rules, share_server=None):
        self._evict_unauthorized_clients(share, access_rules, share_server)
        access_dict = {}
        for access in access_rules:
            try:
                self._allow_access(share, access, share_server)
            except (exception.InfortrendNASException) as e:
                msg = _('Failed to allow access to client %(access)s, '
                        'reason %(e)s.') % {
                            'access': access['access_to'], 'e': e}
                LOG.error(msg)
                access_dict[access['id']] = 'error'

        return access_dict

    def _evict_unauthorized_clients(self, share, access_rules,
                                    share_server=None):
        pool_name = share_utils.extract_host(share['host'], level='pool')
        pool_data = self._get_share_pool_data(pool_name)
        share_proto = share['share_proto'].lower()
        share_name = share['id'].replace('-', '')
        share_path = pool_data['path'] + share_name

        access_list = []
        for access in access_rules:
            access_list.append(access['access_to'])

        if share_proto == 'nfs':
            host_ip_list = []
            command_line = ['share', 'status', '-f', share_path]
            rc, nfs_status = self._execute(command_line)
            host_list = nfs_status[0]['nfs_detail']['hostList']
            for host in host_list:
                if host['host'] != '*':
                    host_ip_list.append(host['host'])
            for ip in host_ip_list:
                if ip not in access_list:
                    command_line = ['share', 'options', share_path,
                                    'nfs', '-c', ip]
                    try:
                        self._execute(command_line)
                    except exception.InfortrendNASException:
                        msg = _("Failed to remove share access rule %s") % (ip)
                        LOG.exception(msg)
                        pass

        elif share_proto == 'cifs':
            host_user_list = []
            command_line = ['acl', 'get', share_path]
            rc, cifs_status = self._execute(command_line)
            for cifs_rule in cifs_status:
                if cifs_rule['name']:
                    host_user_list.append(cifs_rule['name'])
            for user in host_user_list:
                if user not in access_list:
                    command_line = ['acl', 'delete', share_path, '-u', user]
                    try:
                        self._execute(command_line)
                    except exception.InfortrendNASException:
                        msg = _("Failed to remove share access rule %s") % (
                            user)
                        LOG.exception(msg)
                        pass

    def _allow_access(self, share, access, share_server=None):
        pool_name = share_utils.extract_host(share['host'], level='pool')
        pool_data = self._get_share_pool_data(pool_name)
        share_name = share['id'].replace('-', '')
        share_path = pool_data['path'] + share_name
        share_proto = share['share_proto'].lower()
        access_type = access['access_type']
        access_level = access['access_level'] or constants.ACCESS_LEVEL_RW
        access_to = access['access_to']
        ACCESS_LEVEL_MAP = {access_level: access_level}
        msg = self._check_access_legal(share_proto, access_type)
        if msg:
            raise exception.InvalidShareAccess(reason=msg)

        if share_proto == 'nfs':
            command_line = ['share', 'options', share_path, 'nfs',
                            '-h', access_to, '-p', access_level]
            self._execute(command_line)

        elif share_proto == 'cifs':
            if not self._check_user_exist(access_to):
                msg = _('Please create user [%(user)s] in advance.') % {
                    'user': access_to}
                LOG.error(msg)
                raise exception.InfortrendNASException(err=msg)

            if access_level == constants.ACCESS_LEVEL_RW:
                cifs_access = 'f'
            elif access_level == constants.ACCESS_LEVEL_RO:
                cifs_access = 'r'
            try:
                access_level = ACCESS_LEVEL_MAP[access_level]
            except KeyError:
                msg = _('Unsupported access_level: [%s].') % access_level
                raise exception.InvalidInput(msg)

            command_line = ['acl', 'set', share_path,
                            '-u', access_to, '-a', cifs_access]
            self._execute(command_line)

        LOG.info('Share [%(share)s] access to [%(access_to)s] '
                 'level [%(level)s] protocol [%(share_proto)s] completed.', {
                     'share': share['id'],
                     'access_to': access_to,
                     'level': access_level,
                     'share_proto': share_proto})

    def _ensure_protocol_on(self, share_path, share_proto, cifs_name):
        if not self._check_proto_enabled(share_path, share_proto):
            command_line = ['share', share_path, share_proto, 'on']
            if share_proto == 'cifs':
                command_line.extend(['-n', cifs_name])
            self._execute(command_line)

    def _check_proto_enabled(self, share_path, share_proto):
        command_line = ['share', 'status', '-f', share_path]
        rc, share_status = self._execute(command_line)
        if share_status:
            check_enabled = share_status[0][share_proto]
            if check_enabled:
                return True
        return False

    def _check_user_exist(self, user_name):
        command_line = ['useradmin', 'user', 'list']
        rc, user_list = self._execute(command_line)
        for user in user_list:
            if user['Name'] == user_name:
                return True
        return False

    def _check_access_legal(self, share_proto, access_type):
        msg = None
        if share_proto == 'cifs' and access_type != 'user':
            msg = _('Infortrend CIFS share only supports USER access type.')
        elif share_proto == 'nfs' and access_type != 'ip':
            msg = _('Infortrend NFS share only supports IP access type.')
        elif share_proto not in ('nfs', 'cifs'):
            msg = _('Unsupported share protocol [%s].') % share_proto
        return msg

    def get_pool(self, share):
        pool_name = share_utils.extract_host(share['host'], level='pool')
        if not pool_name:
            share_name = share['id'].replace('-', '')
            for pool in self.pool_dict.keys():
                if self._check_share_exist(pool, share_name):
                    pool_name = pool
                    break
        return pool_name

    def ensure_share(self, share, share_server=None):
        share_proto = share['share_proto'].lower()
        pool_name = share_utils.extract_host(share['host'], level='pool')
        pool_data = self._get_share_pool_data(pool_name)
        share_name = share['id'].replace('-', '')
        return self._export_location(
            share_name, share_proto, pool_data['path'])

    def extend_share(self, share, new_size, share_server=None):
        pool_name = share_utils.extract_host(share['host'], level='pool')
        pool_data = self._get_share_pool_data(pool_name)
        share_name = share['id'].replace('-', '')
        self._set_share_size(pool_data['id'], pool_name, share_name, new_size)

        LOG.info('Successfully Extend Share [%(share)s] '
                 'to size [%(new_size)s G].', {
                     'share': share['id'],
                     'new_size': new_size})

    def shrink_share(self, share, new_size, share_server=None):
        pool_name = share_utils.extract_host(share['host'], level='pool')
        pool_data = self._get_share_pool_data(pool_name)
        share_name = share['id'].replace('-', '')
        folder_name = self._extract_lv_name(pool_data)

        command_line = ['fquota', 'status', pool_data['id'],
                        folder_name, '-t', 'folder']
        rc, quota_status = self._execute(command_line)

        for share_quota in quota_status:
            if share_quota['name'] == share_name:
                used_space = round(_bi_to_gi(float(share_quota['used'])), 2)

        if new_size < used_space:
            raise exception.ShareShrinkingPossibleDataLoss(
                share_id=share['id'])

        self._set_share_size(pool_data['id'], pool_name, share_name, new_size)

        LOG.info('Successfully Shrink Share [%(share)s] '
                 'to size [%(new_size)s G].', {
                     'share': share['id'],
                     'new_size': new_size})

    def manage_existing(self, share, driver_options):
        share_proto = share['share_proto'].lower()
        pool_name = share_utils.extract_host(share['host'], level='pool')
        pool_data = self._get_share_pool_data(pool_name)
        volume_name = self._extract_lv_name(pool_data)
        input_location = share['export_locations'][0]['path']
        share_name = share['id'].replace('-', '')

        ch_ip, folder_name = self._parse_location(input_location, share_proto)

        if not self._check_channel_ip(ch_ip):
            msg = _('Export location ip: [%(ch_ip)s] '
                    'is incorrect, please use data port ip.') % {
                        'ch_ip': ch_ip}
            LOG.error(msg)
            raise exception.InfortrendNASException(err=msg)

        if not self._check_share_exist(pool_name, folder_name):
            msg = _('Can not find folder [%(folder_name)s] '
                    'in pool [%(pool_name)s].') % {
                        'folder_name': folder_name,
                        'pool_name': pool_name}
            LOG.error(msg)
            raise exception.InfortrendNASException(err=msg)

        share_path = pool_data['path'] + folder_name
        self._ensure_protocol_on(share_path, share_proto, share_name)
        share_size = self._get_share_size(
            pool_data['id'], pool_name, folder_name)

        if not share_size:
            msg = _('Folder [%(folder_name)s] has no size limitation, '
                    'please set it first for Openstack management.') % {
                        'folder_name': folder_name}
            LOG.error(msg)
            raise exception.InfortrendNASException(err=msg)

        # rename folder name
        command_line = ['folder', 'options', pool_data['id'], volume_name,
                        '-k', folder_name, share_name]
        self._execute(command_line)

        location = self._export_location(
            share_name, share_proto, pool_data['path'])

        LOG.info('Successfully Manage Infortrend Share [%(folder_name)s], '
                 'Size: [%(size)s G], Protocol: [%(share_proto)s], '
                 'new name: [%(share_name)s].', {
                     'folder_name': folder_name,
                     'size': share_size,
                     'share_proto': share_proto,
                     'share_name': share_name})

        return {'size': share_size, 'export_locations': location}

    def _parse_location(self, input_location, share_proto):
        ip = None
        folder_name = None
        pattern_ip = r'[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'
        if share_proto == 'nfs':
            pattern_folder = r'[^\/]+$'
            ip = "".join(re.findall(pattern_ip, input_location))
            folder_name = "".join(re.findall(pattern_folder, input_location))

        elif share_proto == 'cifs':
            pattern_folder = r'[^\\]+$'
            ip = "".join(re.findall(pattern_ip, input_location))
            folder_name = "".join(re.findall(pattern_folder, input_location))

        if not (ip and folder_name):
            msg = _('Export location error, please check '
                    'ip: [%(ip)s], folder_name: [%(folder_name)s].') % {
                        'ip': ip,
                        'folder_name': folder_name}
            LOG.error(msg)
            raise exception.InfortrendNASException(err=msg)

        return ip, folder_name

    def _check_channel_ip(self, channel_ip):
        return any(ip == channel_ip for ip in self.channel_dict.values())

    def unmanage(self, share):
        pool_name = share_utils.extract_host(share['host'], level='pool')
        share_name = share['id'].replace('-', '')

        if not self._check_share_exist(pool_name, share_name):
            LOG.warning('Share [%(share_name)s] does not exist.', {
                'share_name': share_name})
            return

        LOG.info('Successfully Unmanaged Share [%(share)s].', {
            'share': share['id']})
