# Copyright (c) 2014 ProphetStor, 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 errno

from oslo_log import log as logging

from cinder import exception
from cinder.i18n import _
from cinder import interface
from cinder.volume import driver
from cinder.volume.drivers.prophetstor import dplcommon
from cinder.zonemanager import utils as fczm_utils

LOG = logging.getLogger(__name__)


@interface.volumedriver
class DPLFCDriver(dplcommon.DPLCOMMONDriver,
                  driver.FibreChannelDriver):
    def __init__(self, *args, **kwargs):
        super(DPLFCDriver, self).__init__(*args, **kwargs)

    def _get_fc_channel(self):
        """Get FibreChannel info.

        :returns: fcInfos[uuid]
                  fcInfo[uuid]['display_name']
                  fcInfo[uuid]['display_description']
                  fcInfo[uuid]['hardware_address']
                  fcInfo[uuid]['type']
                  fcInfo[uuid]['speed']
                  fcInfo[uuid]['state']
        """
        output = None
        fcInfos = {}
        try:
            retCode, output = self.dpl.get_server_info()
            if retCode == 0 and output:
                fcUuids = output.get('metadata',
                                     {}).get('storage_adapter', {}).keys()
                for fcUuid in fcUuids:
                    fcInfo = output.get('metadata',
                                        {}).get('storage_adapter',
                                                {}).get(fcUuid)
                    if fcInfo['type'] == 'fc':
                        fcInfos[fcUuid] = fcInfo
        except Exception as e:
            LOG.error("Failed to get fiber channel info from storage "
                      "due to %(stat)s", {'stat': e})
        return fcInfos

    def _get_targets(self):
        """Get targets.

        :returns: targetInfos[uuid] = targetInfo
                  targetInfo['targetUuid']
                  targetInfo['targetName']
                  targetInfo['targetAddr']
        """
        output = None
        targetInfos = {}
        try:
            retCode, output = self.dpl.get_target_list('target')
            if retCode == 0 and output:
                for targetInfo in output.get('children', []):
                    targetI = {}
                    targetI['targetUuid'] = targetInfo[0]
                    targetI['targetName'] = targetInfo[1]
                    targetI['targetAddr'] = targetInfo[2]
                    targetInfos[str(targetInfo[0])] = targetI
        except Exception as e:
            targetInfos = {}
            LOG.error("Failed to get fiber channel target from "
                      "storage server due to %(stat)s",
                      {'stat': e})
        return targetInfos

    def _get_targetwpns(self, volumeid, initiatorWwpns):
        lstargetWwpns = []
        try:
            ret, output = self.dpl.get_vdev(volumeid)
            if ret == 0 and output:
                exports = output.get('exports', {})
                fc_infos = exports.get('Network/FC', {})
                for fc_info in fc_infos:
                    for p in fc_info.get('permissions', []):
                        if p.get(initiatorWwpns, None):
                            targetWwpns = fc_info.get('target_identifier', '')
                            lstargetWwpns.append(targetWwpns)
        except Exception as e:
            LOG.error("Failed to get target wwpns from storage due "
                      "to %(stat)s", {'stat': e})
            lstargetWwpns = []
        return lstargetWwpns

    def _is_initiator_wwpn_active(self, targetWwpn, initiatorWwpn):
        fActive = False
        output = None
        try:
            retCode, output = self.dpl.get_sns_table(targetWwpn)
            if retCode == 0 and output:
                for fdwwpn, fcport in output.get('metadata',
                                                 {}).get('sns_table',
                                                         []):
                    if fdwwpn == initiatorWwpn:
                        fActive = True
                        break
        except Exception:
            LOG.error('Failed to get sns table')
        return fActive

    def _convertHex2String(self, wwpns):
        szwwpns = ''
        if len(str(wwpns)) == 16:
            szwwpns = '%2s:%2s:%2s:%2s:%2s:%2s:%2s:%2s' % (
                str(wwpns)[0:2],
                str(wwpns)[2:4],
                str(wwpns)[4:6],
                str(wwpns)[6:8],
                str(wwpns)[8:10],
                str(wwpns)[10:12],
                str(wwpns)[12:14],
                str(wwpns)[14:16])
        return szwwpns

    def _export_fc(self, volumeid, targetwwpns, initiatorwwpns, volumename):
        ret = 0
        output = ''
        LOG.debug('Export fc: %(volume)s, %(wwpns)s, %(iqn)s, %(volumename)s',
                  {'volume': volumeid, 'wwpns': targetwwpns,
                   'iqn': initiatorwwpns, 'volumename': volumename})
        try:
            ret, output = self.dpl.assign_vdev_fc(
                self._conver_uuid2hex(volumeid), targetwwpns,
                initiatorwwpns, volumename)
        except Exception:
            LOG.error('Volume %(volumeid)s failed to send assign command, '
                      'ret: %(status)s output: %(output)s',
                      {'volumeid': volumeid, 'status': ret, 'output': output})
            ret = errno.EFAULT

        if ret == errno.EAGAIN:
            ret, event_uuid = self._get_event_uuid(output)
            if len(event_uuid):
                ret = 0
                status = self._wait_event(
                    self.dpl.get_vdev_status,
                    self._conver_uuid2hex(volumeid), event_uuid)
                if status['state'] == 'error':
                    ret = errno.EFAULT
                    msg = _('Flexvisor failed to assign volume %(id)s: '
                            '%(status)s.') % {'id': volumeid,
                                              'status': status}
                    raise exception.VolumeBackendAPIException(data=msg)
            else:
                ret = errno.EFAULT
                msg = _('Flexvisor failed to assign volume %(id)s due to '
                        'unable to query status by event '
                        'id.') % {'id': volumeid}
                raise exception.VolumeBackendAPIException(data=msg)
        elif ret != 0:
            msg = _('Flexvisor assign volume failed:%(id)s:'
                    '%(status)s.') % {'id': volumeid, 'status': ret}
            raise exception.VolumeBackendAPIException(data=msg)

        return ret

    def _delete_export_fc(self, volumeid, targetwwpns, initiatorwwpns):
        ret = 0
        output = ''
        ret, output = self.dpl.unassign_vdev_fc(
            self._conver_uuid2hex(volumeid),
            targetwwpns, initiatorwwpns)
        if ret == errno.EAGAIN:
            ret, event_uuid = self._get_event_uuid(output)
            if ret == 0 and len(event_uuid):
                status = self._wait_event(
                    self.dpl.get_vdev_status, volumeid, event_uuid)
                if status['state'] == 'error':
                    ret = errno.EFAULT
                    msg = _('Flexvisor failed to unassign volume %(id)s:'
                            ' %(status)s.') % {'id': volumeid,
                                               'status': status}
                    raise exception.VolumeBackendAPIException(data=msg)
            else:
                msg = _('Flexvisor failed to unassign volume (get event) '
                        '%(id)s.') % {'id': volumeid}
                raise exception.VolumeBackendAPIException(data=msg)
        elif ret != 0:
            msg = _('Flexvisor unassign volume failed:%(id)s:'
                    '%(status)s.') % {'id': volumeid, 'status': ret}
            raise exception.VolumeBackendAPIException(data=msg)
        else:
            LOG.info('Flexvisor succeeded to unassign volume %(id)s.',
                     {'id': volumeid})

        return ret

    def _build_initiator_target_map(self, connector, tgtwwns):
        """Build the target_wwns and the initiator target map."""
        init_targ_map = {}
        initiator_wwns = connector['wwpns']
        for initiator in initiator_wwns:
            init_targ_map[initiator] = tgtwwns

        return init_targ_map

    def initialize_connection(self, volume, connector):
        """Allow connection to connector and return connection info."""
        """
            connector = {'ip': CONF.my_ip,
                         'host': CONF.host,
                         'initiator': self._initiator,
                         'wwnns': self._fc_wwnns,
                         'wwpns': self._fc_wwpns}

        """
        dc_fc = {}
        dc_target = {}
        lsTargetWwpn = []
        output = None
        properties = {}
        preferTargets = {}
        ret = 0
        targetIdentifier = []
        szwwpns = []
        LOG.info('initialize_connection volume: %(volume)s, connector:'
                 ' %(connector)s',
                 {"volume": volume, "connector": connector})
        # Get Storage Fiber channel controller
        dc_fc = self._get_fc_channel()

        # Get existed FC target list to decide target wwpn
        dc_target = self._get_targets()
        if len(dc_target) == 0:
            msg = _('Backend storage did not configure fiber channel '
                    'target.')
            raise exception.VolumeBackendAPIException(data=msg)

        for keyFc in dc_fc:
            for targetuuid in dc_target:
                if dc_fc[keyFc]['hardware_address'] == \
                        dc_target[targetuuid]['targetAddr']:
                    preferTargets[targetuuid] = dc_target[targetuuid]
                    break
        # Confirm client wwpn is existed in sns table
        # Covert wwwpns to 'xx:xx:xx:xx:xx:xx:xx:xx' format
        for dwwpn in connector['wwpns']:
            szwwpn = self._convertHex2String(dwwpn)
            if len(szwwpn) == 0:
                msg = _('Invalid wwpns format %(wwpns)s') % \
                    {'wwpns': connector['wwpns']}
                raise exception.VolumeBackendAPIException(data=msg)
            szwwpns.append(szwwpn)

        if len(szwwpns):
            for targetUuid in preferTargets:
                targetWwpn = ''
                targetWwpn = preferTargets.get(targetUuid,
                                               {}).get('targetAddr', '')
                lsTargetWwpn.append(targetWwpn)
        # Use wwpns to assign volume.
        LOG.info('Prefer use target wwpn %(wwpn)s',
                 {'wwpn': lsTargetWwpn})
        # Start to create export in all FC target node.
        assignedTarget = []
        for pTarget in lsTargetWwpn:
            try:
                ret = self._export_fc(volume['id'], str(pTarget), szwwpns,
                                      volume['name'])
                if ret:
                    break
                else:
                    assignedTarget.append(pTarget)
            except Exception as e:
                LOG.error('Failed to export fiber channel target '
                          'due to %s', e)
                ret = errno.EFAULT
                break
        if ret == 0:
            ret, output = self.dpl.get_vdev(self._conver_uuid2hex(
                volume['id']))
        nLun = -1
        if ret == 0:
            try:
                for p in output['exports']['Network/FC']:
                    # check initiator wwpn existed in target initiator list
                    for initI in p.get('permissions', []):
                        for szwpn in szwwpns:
                            if initI.get(szwpn, None):
                                nLun = initI[szwpn]
                                break
                        if nLun != -1:
                            break

                    if nLun != -1:
                        targetIdentifier.append(
                            str(p['target_identifier']).replace(':', ''))

            except Exception:
                msg = _('Invalid connection initialization response of '
                        'volume %(name)s: '
                        '%(output)s') % {'name': volume['name'],
                                         'output': output}
                raise exception.VolumeBackendAPIException(data=msg)

        if nLun != -1:
            init_targ_map = self._build_initiator_target_map(connector,
                                                             targetIdentifier)
            properties['target_discovered'] = True
            properties['target_wwn'] = targetIdentifier
            properties['target_lun'] = int(nLun)
            properties['volume_id'] = volume['id']
            properties['initiator_target_map'] = init_targ_map
            LOG.info('%(volume)s assign type fibre_channel, properties '
                     '%(properties)s',
                     {'volume': volume['id'], 'properties': properties})
        else:
            msg = _('Invalid connection initialization response of '
                    'volume %(name)s') % {'name': volume['name']}
            raise exception.VolumeBackendAPIException(data=msg)
        LOG.info('Connect initialization info: '
                 '{driver_volume_type: fibre_channel, '
                 'data: %(properties)s', {'properties': properties})
        conn_info = {'driver_volume_type': 'fibre_channel',
                     'data': properties}
        fczm_utils.add_fc_zone(conn_info)
        return conn_info

    def terminate_connection(self, volume, connector, **kwargs):
        """Disallow connection from connector."""
        """
            connector = {'ip': CONF.my_ip,
                         'host': CONF.host,
                         'initiator': self._initiator,
                         'wwnns': self._fc_wwnns,
                         'wwpns': self._fc_wwpns}
        """
        lstargetWwpns = []
        lsTargets = []
        szwwpns = []
        ret = 0
        info = {'driver_volume_type': 'fibre_channel', 'data': {}}
        LOG.info('terminate_connection volume: %(volume)s, '
                 'connector: %(con)s',
                 {'volume': volume, 'con': connector})
        # Query targetwwpns.
        # Get all target list of volume.
        for dwwpn in connector['wwpns']:
            szwwpn = self._convertHex2String(dwwpn)
            if len(szwwpn) == 0:
                msg = _('Invalid wwpns format %(wwpns)s') % \
                    {'wwpns': connector['wwpns']}
                raise exception.VolumeBackendAPIException(data=msg)
            szwwpns.append(szwwpn)

        if len(szwwpns) == 0:
            ret = errno.EFAULT
            msg = _('Invalid wwpns format %(wwpns)s') % \
                {'wwpns': connector['wwpns']}
            raise exception.VolumeBackendAPIException(data=msg)
        else:
            for szwwpn in szwwpns:
                lstargetWwpns = self._get_targetwpns(
                    self._conver_uuid2hex(volume['id']), szwwpn)
                lsTargets = list(set(lsTargets + lstargetWwpns))

        # Remove all export target
        try:
            for ptarget in lsTargets:
                ret = self._delete_export_fc(volume['id'], ptarget, szwwpns)
                if ret:
                    break
        except Exception:
            ret = errno.EFAULT
        finally:
            if ret:
                msg = _('Faield to unassign %(volume)s') % (volume['id'])
                raise exception.VolumeBackendAPIException(data=msg)

        # Failed to delete export with fibre channel
        if ret:
            init_targ_map = self._build_initiator_target_map(connector,
                                                             lsTargets)
            info['data'] = {'target_wwn': lsTargets,
                            'initiator_target_map': init_targ_map}
            fczm_utils.remove_fc_zone(info)

        return info

    def get_volume_stats(self, refresh=False):
        if refresh:
            data = super(DPLFCDriver, self).get_volume_stats(refresh)
            if data:
                data['storage_protocol'] = 'FC'
                backend_name = \
                    self.configuration.safe_get('volume_backend_name')
                data['volume_backend_name'] = (backend_name or 'DPLFCDriver')
                self._stats = data
        return self._stats
