# Copyright (c) 2013 Huawei Technologies Co., Ltd.
# Copyright (c) 2012 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.
"""
Volume Drivers for Huawei OceanStor T series storage arrays.
"""

import re
import time

from cinder import exception
from cinder.i18n import _
from cinder.openstack.common import log as logging
from cinder.volume import driver
from cinder.volume.drivers.huawei import huawei_utils
from cinder.volume.drivers.huawei import ssh_common
from cinder.zonemanager import utils as fczm_utils


LOG = logging.getLogger(__name__)

HOST_PORT_PREFIX = 'HostPort_'


class HuaweiTISCSIDriver(driver.ISCSIDriver):
    """ISCSI driver for Huawei OceanStor T series storage arrays."""

    VERSION = '1.1.0'

    def __init__(self, *args, **kwargs):
        super(HuaweiTISCSIDriver, self).__init__(*args, **kwargs)

    def do_setup(self, context):
        """Instantiate common class."""
        self.common = ssh_common.TseriesCommon(configuration=
                                               self.configuration)
        self.common.do_setup(context)
        self._assert_cli_out = self.common._assert_cli_out
        self._assert_cli_operate_out = self.common._assert_cli_operate_out

    def check_for_setup_error(self):
        """Check something while starting."""
        self.common.check_for_setup_error()

    def create_volume(self, volume):
        """Create a new volume."""
        volume_id = self.common.create_volume(volume)
        return {'provider_location': volume_id}

    def create_volume_from_snapshot(self, volume, snapshot):
        """Create a volume from a snapshot."""
        volume_id = self.common.create_volume_from_snapshot(volume, snapshot)
        return {'provider_location': volume_id}

    def create_cloned_volume(self, volume, src_vref):
        """Create a clone of the specified volume."""
        volume_id = self.common.create_cloned_volume(volume, src_vref)
        return {'provider_location': volume_id}

    def extend_volume(self, volume, new_size):
        """Extend a volume."""
        self.common.extend_volume(volume, new_size)

    def delete_volume(self, volume):
        """Delete a volume."""
        self.common.delete_volume(volume)

    def create_export(self, context, volume):
        """Export the volume."""
        pass

    def ensure_export(self, context, volume):
        """Synchronously recreate an export for a volume."""
        pass

    def remove_export(self, context, volume):
        """Remove an export for a volume."""
        pass

    def create_snapshot(self, snapshot):
        """Create a snapshot."""
        snapshot_id = self.common.create_snapshot(snapshot)
        return {'provider_location': snapshot_id}

    def delete_snapshot(self, snapshot):
        """Delete a snapshot."""
        self.common.delete_snapshot(snapshot)

    def initialize_connection(self, volume, connector):
        """Map a volume to a host and return target iSCSI information."""
        LOG.debug('initialize_connection: volume name: %(vol)s, '
                  'host: %(host)s, initiator: %(ini)s'
                  % {'vol': volume['name'],
                     'host': connector['host'],
                     'ini': connector['initiator']})

        self.common._update_login_info()
        (iscsi_iqn, target_ip, port_ctr) =\
            self._get_iscsi_params(connector['initiator'])

        # First, add a host if not added before.
        host_id = self.common.add_host(connector['host'], connector['ip'],
                                       connector['initiator'])

        # Then, add the iSCSI port to the host.
        self._add_iscsi_port_to_host(host_id, connector)

        # Finally, map the volume to the host.
        volume_id = volume['provider_location']
        hostlun_id = self.common.map_volume(host_id, volume_id)

        # Change LUN ctr for better performance, just for single path.
        lun_details = self.common.get_lun_details(volume_id)
        if (lun_details['LunType'] == 'THICK' and
                lun_details['OwningController'] != port_ctr):
            self.common.change_lun_ctr(volume_id, port_ctr)

        properties = {}
        properties['target_discovered'] = False
        properties['target_portal'] = ('%s:%s' % (target_ip, '3260'))
        properties['target_iqn'] = iscsi_iqn
        properties['target_lun'] = int(hostlun_id)
        properties['volume_id'] = volume['id']
        auth = volume['provider_auth']
        if auth:
            (auth_method, auth_username, auth_secret) = auth.split()

            properties['auth_method'] = auth_method
            properties['auth_username'] = auth_username
            properties['auth_password'] = auth_secret

        return {'driver_volume_type': 'iscsi', 'data': properties}

    def _get_iscsi_params(self, initiator):
        """Get target iSCSI params, including iqn and IP."""
        conf_file = self.common.configuration.cinder_huawei_conf_file
        iscsi_conf = self._get_iscsi_conf(conf_file)
        target_ip = None
        for ini in iscsi_conf['Initiator']:
            if ini['Name'] == initiator:
                target_ip = ini['TargetIP']
                break
        # If didn't specify target IP for some initiator, use default IP.
        if not target_ip:
            if iscsi_conf['DefaultTargetIP']:
                target_ip = iscsi_conf['DefaultTargetIP']

            else:
                msg = (_('_get_iscsi_params: Failed to get target IP '
                         'for initiator %(ini)s, please check config file.')
                       % {'ini': initiator})
                LOG.error(msg)
                raise exception.InvalidInput(reason=msg)

        (target_iqn, port_ctr) = self._get_tgt_iqn(target_ip)
        return (target_iqn, target_ip, port_ctr)

    def _get_iscsi_conf(self, filename):
        """Get iSCSI info from config file.

        This function returns a dict:
        {'DefaultTargetIP': '11.11.11.11',
         'Initiator': [{'Name': 'iqn.xxxxxx.1', 'TargetIP': '11.11.11.12'},
                       {'Name': 'iqn.xxxxxx.2', 'TargetIP': '11.11.11.13'}
                      ]
        }

        """

        iscsiinfo = {}
        root = huawei_utils.parse_xml_file(filename)

        default_ip = root.findtext('iSCSI/DefaultTargetIP')
        if default_ip:
            iscsiinfo['DefaultTargetIP'] = default_ip.strip()
        else:
            iscsiinfo['DefaultTargetIP'] = None
        initiator_list = []
        tmp_dic = {}
        for dic in root.findall('iSCSI/Initiator'):
            # Strip the values of dict.
            for k, v in dic.items():
                tmp_dic[k] = v.strip()
            initiator_list.append(tmp_dic)
        iscsiinfo['Initiator'] = initiator_list
        return iscsiinfo

    def _get_tgt_iqn(self, port_ip):
        """Run CLI command to get target iSCSI iqn.

        The iqn is formed with three parts:
        iSCSI target name + iSCSI port info + iSCSI IP

        """

        LOG.debug('_get_tgt_iqn: iSCSI IP is %s.' % port_ip)

        cli_cmd = 'showiscsitgtname'
        out = self.common._execute_cli(cli_cmd)

        self._assert_cli_out(re.search('ISCSI Name', out),
                             '_get_tgt_iqn',
                             'Failed to get iSCSI target %s iqn.' % port_ip,
                             cli_cmd, out)

        lines = out.split('\r\n')
        index = lines[4].index('iqn')
        iqn_prefix = lines[4][index:].strip()
        # Here we make sure port_info won't be None.
        port_info = self._get_iscsi_tgt_port_info(port_ip)
        ctr = ('0' if port_info[0] == 'A' else '1')
        interface = '0' + port_info[1]
        port = '0' + port_info[2][1:]
        iqn_suffix = ctr + '02' + interface + port
        # iqn_suffix should not start with 0
        while(True):
            if iqn_suffix.startswith('0'):
                iqn_suffix = iqn_suffix[1:]
            else:
                break

        iqn = iqn_prefix + ':' + iqn_suffix + ':' + port_info[3]

        LOG.debug('_get_tgt_iqn: iSCSI target iqn is %s.' % iqn)

        return (iqn, port_info[0])

    def _get_iscsi_tgt_port_info(self, port_ip):
        """Get iSCSI Port information of storage device."""
        cli_cmd = 'showiscsiip'
        out = self.common._execute_cli(cli_cmd)
        if re.search('iSCSI IP Information', out):
            for line in out.split('\r\n')[6:-2]:
                tmp_line = line.split()
                if tmp_line[3] == port_ip:
                    return tmp_line

        err_msg = _('_get_iscsi_tgt_port_info: Failed to get iSCSI port '
                    'info. Please make sure the iSCSI port IP %s is '
                    'configured in array.') % port_ip
        LOG.error(err_msg)
        raise exception.VolumeBackendAPIException(data=err_msg)

    def _add_iscsi_port_to_host(self, hostid, connector, multipathtype=0):
        """Add an iSCSI port to the given host.

        First, add an initiator if needed, the initiator is equivalent to
        an iSCSI port. Then, add the initiator to host if not added before.

        """

        initiator = connector['initiator']
        # Add an iSCSI initiator.
        if not self._initiator_added(initiator):
            self._add_initiator(initiator)
        # Add the initiator to host if not added before.
        port_name = HOST_PORT_PREFIX + str(hash(initiator))
        portadded = False
        hostport_info = self.common._get_host_port_info(hostid)
        if hostport_info:
            for hostport in hostport_info:
                if hostport[2] == initiator:
                    portadded = True
                    break
        if not portadded:
            cli_cmd = ('addhostport -host %(id)s -type 5 '
                       '-info %(info)s -n %(name)s -mtype %(multype)s'
                       % {'id': hostid,
                          'info': initiator,
                          'name': port_name,
                          'multype': multipathtype})
            out = self.common._execute_cli(cli_cmd)

            msg = ('Failed to add iSCSI port %(port)s to host %(host)s'
                   % {'port': port_name,
                      'host': hostid})
            self._assert_cli_operate_out('_add_iscsi_port_to_host',
                                         msg, cli_cmd, out)

    def _initiator_added(self, ininame):
        """Check whether the initiator is already added."""
        cli_cmd = 'showiscsiini -ini %(name)s' % {'name': ininame}
        out = self.common._execute_cli(cli_cmd)
        return (True if re.search('Initiator Information', out) else False)

    def _add_initiator(self, ininame):
        """Add a new initiator to storage device."""
        cli_cmd = 'addiscsiini -n %(name)s' % {'name': ininame}
        out = self.common._execute_cli(cli_cmd)

        self._assert_cli_operate_out('_add_iscsi_host_port',
                                     'Failed to add initiator %s' % ininame,
                                     cli_cmd, out)

    def _delete_initiator(self, ininame, attempts=2):
        """Delete an initiator."""
        cli_cmd = 'deliscsiini -n %(name)s' % {'name': ininame}
        while(attempts > 0):
            out = self.common._execute_cli(cli_cmd)
            if re.search('the port is in use', out):
                attempts -= 1
                time.sleep(2)
            else:
                break

        self._assert_cli_operate_out('_map_lun',
                                     'Failed to delete initiator %s.'
                                     % ininame,
                                     cli_cmd, out)

    def terminate_connection(self, volume, connector, **kwargs):
        """Terminate the map."""
        LOG.debug('terminate_connection: volume: %(vol)s, host: %(host)s, '
                  'connector: %(initiator)s'
                  % {'vol': volume['name'],
                     'host': connector['host'],
                     'initiator': connector['initiator']})

        self.common._update_login_info()
        host_id = self.common.remove_map(volume['provider_location'],
                                         connector['host'],
                                         connector['initiator'])
        if not self.common._get_host_map_info(host_id):
            self._remove_iscsi_port(host_id, connector)

    def _remove_iscsi_port(self, hostid, connector):
        """Remove iSCSI ports and delete host."""
        initiator = connector['initiator']
        # Delete the host initiator if no LUN mapped to it.
        port_num = 0
        port_info = self.common._get_host_port_info(hostid)
        if port_info:
            port_num = len(port_info)
            for port in port_info:
                if port[2] == initiator:
                    self.common._delete_hostport(port[0])
                    self._delete_initiator(initiator)
                    port_num -= 1
                    break
        else:
            LOG.warn(_('_remove_iscsi_port: iSCSI port was not found '
                       'on host %(hostid)s.') % {'hostid': hostid})

        # Delete host if no initiator added to it.
        if port_num == 0:
            self.common._delete_host(hostid)

    def get_volume_stats(self, refresh=False):
        """Get volume stats."""
        self._stats = self.common.get_volume_stats(refresh)
        self._stats['storage_protocol'] = 'iSCSI'
        self._stats['driver_version'] = self.VERSION
        backend_name = self.configuration.safe_get('volume_backend_name')
        self._stats['volume_backend_name'] = (backend_name or
                                              self.__class__.__name__)
        return self._stats


class HuaweiTFCDriver(driver.FibreChannelDriver):
    """FC driver for Huawei OceanStor T series storage arrays."""

    VERSION = '1.0.0'

    def __init__(self, *args, **kwargs):
        super(HuaweiTFCDriver, self).__init__(*args, **kwargs)

    def do_setup(self, context):
        """Instantiate common class."""
        self.common = ssh_common.TseriesCommon(configuration=
                                               self.configuration)
        self.common.do_setup(context)
        self._assert_cli_out = self.common._assert_cli_out
        self._assert_cli_operate_out = self.common._assert_cli_operate_out

    def check_for_setup_error(self):
        """Check something while starting."""
        self.common.check_for_setup_error()

    def create_volume(self, volume):
        """Create a new volume."""
        volume_id = self.common.create_volume(volume)
        return {'provider_location': volume_id}

    def create_volume_from_snapshot(self, volume, snapshot):
        """Create a volume from a snapshot."""
        volume_id = self.common.create_volume_from_snapshot(volume, snapshot)
        return {'provider_location': volume_id}

    def create_cloned_volume(self, volume, src_vref):
        """Create a clone of the specified volume."""
        volume_id = self.common.create_cloned_volume(volume, src_vref)
        return {'provider_location': volume_id}

    def extend_volume(self, volume, new_size):
        """Extend a volume."""
        self.common.extend_volume(volume, new_size)

    def delete_volume(self, volume):
        """Delete a volume."""
        self.common.delete_volume(volume)

    def create_export(self, context, volume):
        """Export the volume."""
        pass

    def ensure_export(self, context, volume):
        """Synchronously recreate an export for a volume."""
        pass

    def remove_export(self, context, volume):
        """Remove an export for a volume."""
        pass

    def create_snapshot(self, snapshot):
        """Create a snapshot."""
        snapshot_id = self.common.create_snapshot(snapshot)
        return {'provider_location': snapshot_id}

    def delete_snapshot(self, snapshot):
        """Delete a snapshot."""
        self.common.delete_snapshot(snapshot)

    def validate_connector(self, connector):
        """Check for wwpns in connector."""
        if 'wwpns' not in connector:
            err_msg = (_('validate_connector: The FC driver requires the'
                         ' wwpns in the connector.'))
            LOG.error(err_msg)
            raise exception.VolumeBackendAPIException(data=err_msg)

    @fczm_utils.AddFCZone
    def initialize_connection(self, volume, connector):
        """Create FC connection between a volume and a host."""
        LOG.debug('initialize_connection: volume name: %(vol)s, '
                  'host: %(host)s, initiator: %(wwn)s'
                  % {'vol': volume['name'],
                     'host': connector['host'],
                     'wwn': connector['wwpns']})

        self.common._update_login_info()
        # First, add a host if it is not added before.
        host_id = self.common.add_host(connector['host'], connector['ip'])
        # Then, add free FC ports to the host.
        ini_wwns = connector['wwpns']
        free_wwns = self._get_connected_free_wwns()
        for wwn in free_wwns:
            if wwn in ini_wwns:
                self._add_fc_port_to_host(host_id, wwn)
        fc_port_details = self._get_host_port_details(host_id)
        tgt_wwns = self._get_tgt_fc_port_wwns(fc_port_details)

        LOG.debug('initialize_connection: Target FC ports WWNS: %s'
                  % tgt_wwns)

        # Finally, map the volume to the host.
        volume_id = volume['provider_location']
        hostlun_id = self.common.map_volume(host_id, volume_id)

        # Change LUN ctr for better performance, just for single path.
        if len(tgt_wwns) == 1:
            lun_details = self.common.get_lun_details(volume_id)
            port_ctr = self._get_fc_port_ctr(fc_port_details[0])
            if (lun_details['LunType'] == 'THICK' and
                    lun_details['OwningController'] != port_ctr):
                self.common.change_lun_ctr(volume_id, port_ctr)

        properties = {}
        properties['target_discovered'] = False
        properties['target_wwn'] = tgt_wwns
        properties['target_lun'] = int(hostlun_id)
        properties['volume_id'] = volume['id']

        return {'driver_volume_type': 'fibre_channel',
                'data': properties}

    def _get_connected_free_wwns(self):
        """Get free connected FC port WWNs.

        If no new ports connected, return an empty list.

        """

        cli_cmd = 'showfreeport'
        out = self.common._execute_cli(cli_cmd)
        wwns = []
        if re.search('Host Free Port Information', out):
            for line in out.split('\r\n')[6:-2]:
                tmp_line = line.split()
                if (tmp_line[1] == 'FC') and (tmp_line[4] == 'Connected'):
                    wwns.append(tmp_line[0])

        return wwns

    def _add_fc_port_to_host(self, hostid, wwn, multipathtype=0):
        """Add a FC port to host."""
        portname = HOST_PORT_PREFIX + wwn
        cli_cmd = ('addhostport -host %(id)s -type 1 '
                   '-wwn %(wwn)s -n %(name)s -mtype %(multype)s'
                   % {'id': hostid,
                      'wwn': wwn,
                      'name': portname,
                      'multype': multipathtype})
        out = self.common._execute_cli(cli_cmd)

        msg = ('Failed to add FC port %(port)s to host %(host)s.'
               % {'port': portname, 'host': hostid})
        self._assert_cli_operate_out('_add_fc_port_to_host', msg, cli_cmd, out)

    def _get_host_port_details(self, host_id):
        cli_cmd = 'showhostpath -host %s' % host_id
        out = self.common._execute_cli(cli_cmd)

        self._assert_cli_out(re.search('Multi Path Information', out),
                             '_get_host_port_details',
                             'Failed to get host port details.',
                             cli_cmd, out)

        port_details = []
        tmp_details = {}
        for line in out.split('\r\n')[4:-2]:
            line = line.split('|')
            # Cut-point of multipal details, usually is "-------".
            if len(line) == 1:
                port_details.append(tmp_details)
                continue
            key = ''.join(line[0].strip().split())
            val = line[1].strip()
            tmp_details[key] = val
        port_details.append(tmp_details)
        return port_details

    def _get_tgt_fc_port_wwns(self, port_details):
        wwns = []
        for port in port_details:
            wwns.append(port['TargetWWN'])
        return wwns

    def _get_fc_port_ctr(self, port_details):
        return port_details['ControllerID']

    @fczm_utils.RemoveFCZone
    def terminate_connection(self, volume, connector, **kwargs):
        """Terminate the map."""
        LOG.debug('terminate_connection: volume: %(vol)s, host: %(host)s, '
                  'connector: %(initiator)s'
                  % {'vol': volume['name'],
                     'host': connector['host'],
                     'initiator': connector['initiator']})

        self.common._update_login_info()
        host_id = self.common.remove_map(volume['provider_location'],
                                         connector['host'])
        # Remove all FC ports and delete the host if
        # no volume mapping to it.
        if not self.common._get_host_map_info(host_id):
            self._remove_fc_ports(host_id, connector)

    def _remove_fc_ports(self, hostid, connector):
        """Remove FC ports and delete host."""
        wwns = connector['wwpns']
        port_num = 0
        port_info = self.common._get_host_port_info(hostid)
        if port_info:
            port_num = len(port_info)
            for port in port_info:
                if port[2] in wwns:
                    self.common._delete_hostport(port[0])
                    port_num -= 1
        else:
            LOG.warn(_('_remove_fc_ports: FC port was not found '
                       'on host %(hostid)s.') % {'hostid': hostid})

        if port_num == 0:
            self.common._delete_host(hostid)

    def get_volume_stats(self, refresh=False):
        """Get volume stats."""
        self._stats = self.common.get_volume_stats(refresh)
        self._stats['storage_protocol'] = 'FC'
        self._stats['driver_version'] = self.VERSION
        backend_name = self.configuration.safe_get('volume_backend_name')
        self._stats['volume_backend_name'] = (backend_name or
                                              self.__class__.__name__)
        return self._stats
