# Copyright 2015 IBM Corp.
# 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 driver for IBM FlashSystem storage systems with FC protocol.

Limitations:
1. Cinder driver only works when open_access_enabled=off.

"""

import random
import threading

from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
import six

from cinder import exception
from cinder.i18n import _
from cinder import interface
from cinder import utils
from cinder.volume import configuration
from cinder.volume.drivers.ibm import flashsystem_common as fscommon
from cinder.volume.drivers.san import san
from cinder.zonemanager import utils as fczm_utils

LOG = logging.getLogger(__name__)

flashsystem_fc_opts = [
    cfg.BoolOpt('flashsystem_multipath_enabled',
                default=False,
                help='This option no longer has any affect. It is deprecated '
                     'and will be removed in the next release.',
                deprecated_for_removal=True)
]

CONF = cfg.CONF
CONF.register_opts(flashsystem_fc_opts, group=configuration.SHARED_CONF_GROUP)


@interface.volumedriver
class FlashSystemFCDriver(fscommon.FlashSystemDriver):
    """IBM FlashSystem FC volume driver.

    Version history:

    .. code-block:: none

        1.0.0 - Initial driver
        1.0.1 - Code clean up
        1.0.2 - Add lock into vdisk map/unmap, connection
                initialize/terminate
        1.0.3 - Initial driver for iSCSI
        1.0.4 - Split Flashsystem driver into common and FC
        1.0.5 - Report capability of volume multiattach
        1.0.6 - Fix bug #1469581, add I/T mapping check in
                terminate_connection
        1.0.7 - Fix bug #1505477, add host name check in
                _find_host_exhaustive for FC
        1.0.8 - Fix bug #1572743, multi-attach attribute
                should not be hardcoded, only in iSCSI
        1.0.9 - Fix bug #1570574, Cleanup host resource
                leaking, changes only in iSCSI
        1.0.10 - Fix bug #1585085, add host name check in
                 _find_host_exhaustive for iSCSI
        1.0.11 - Update driver to use ABC metaclasses
        1.0.12 - Update driver to support Manage/Unmanage
                 existing volume
    """

    VERSION = "1.0.12"

    # ThirdPartySystems wiki page
    CI_WIKI_NAME = "IBM_STORAGE_CI"

    def __init__(self, *args, **kwargs):
        super(FlashSystemFCDriver, self).__init__(*args, **kwargs)
        self.configuration.append_config_values(fscommon.flashsystem_opts)
        self.configuration.append_config_values(flashsystem_fc_opts)
        self.configuration.append_config_values(san.san_opts)

    def _check_vdisk_params(self, params):
        # Check that the requested protocol is enabled
        if params['protocol'] != self._protocol:
            msg = (_("Illegal value '%(prot)s' specified for "
                     "flashsystem_connection_protocol: "
                     "valid value(s) are %(enabled)s.")
                   % {'prot': params['protocol'],
                      'enabled': self._protocol})
            raise exception.InvalidInput(reason=msg)

    def _create_host(self, connector):
        """Create a new host on the storage system.

        We create a host and associate it with the given connection
        information.

        """

        LOG.debug('enter: _create_host: host %s.', connector['host'])

        rand_id = six.text_type(random.randint(0, 99999999)).zfill(8)
        host_name = '%s-%s' % (self._connector_to_hostname_prefix(connector),
                               rand_id)

        ports = []
        if 'FC' == self._protocol and 'wwpns' in connector:
            for wwpn in connector['wwpns']:
                ports.append('-hbawwpn %s' % wwpn)

        self._driver_assert(ports,
                            (_('_create_host: No connector ports.')))
        port1 = ports.pop(0)
        arg_name, arg_val = port1.split()
        ssh_cmd = ['svctask', 'mkhost', '-force', arg_name, arg_val, '-name',
                   '"%s"' % host_name]
        out, err = self._ssh(ssh_cmd)
        self._assert_ssh_return('successfully created' in out,
                                '_create_host', ssh_cmd, out, err)

        for port in ports:
            arg_name, arg_val = port.split()
            ssh_cmd = ['svctask', 'addhostport', '-force',
                       arg_name, arg_val, host_name]
            out, err = self._ssh(ssh_cmd)
            self._assert_ssh_return(
                (not out.strip()),
                '_create_host', ssh_cmd, out, err)

        LOG.debug(
            'leave: _create_host: host %(host)s - %(host_name)s.',
            {'host': connector['host'], 'host_name': host_name})

        return host_name

    def _find_host_exhaustive(self, connector, hosts):
        hname = connector['host']
        hnames = [ihost[0:ihost.rfind('-')] for ihost in hosts]
        if hname in hnames:
            host = hosts[hnames.index(hname)]
            ssh_cmd = ['svcinfo', 'lshost', '-delim', '!', host]
            out, err = self._ssh(ssh_cmd)
            self._assert_ssh_return(
                out.strip(),
                '_find_host_exhaustive', ssh_cmd, out, err)
            attr_lines = [attr_line for attr_line in out.split('\n')]
            attr_parm = {}
            for attr_line in attr_lines:
                attr_name, foo, attr_val = attr_line.partition('!')
                attr_parm[attr_name] = attr_val
            if ('WWPN' in attr_parm.keys() and 'wwpns' in connector and
                    attr_parm['WWPN'].lower() in
                    map(str.lower, map(str, connector['wwpns']))):
                return host
        else:
            LOG.warning('Host %(host)s was not found on backend storage.',
                        {'host': hname})
        return None

    def _get_conn_fc_wwpns(self):
        wwpns = []

        cmd = ['svcinfo', 'lsportfc']

        generator = self._port_conf_generator(cmd)
        header = next(generator, None)
        if not header:
            return wwpns

        for port_data in generator:
            try:
                if port_data['status'] == 'active':
                    wwpns.append(port_data['WWPN'])
            except KeyError:
                self._handle_keyerror('lsportfc', header)

        return wwpns

    def _get_fc_wwpns(self):
        for key in self._storage_nodes:
            node = self._storage_nodes[key]
            ssh_cmd = ['svcinfo', 'lsnode', '-delim', '!', node['id']]
            attributes = self._execute_command_and_parse_attributes(ssh_cmd)
            wwpns = set(node['WWPN'])
            for i, s in zip(attributes['port_id'], attributes['port_status']):
                if 'unconfigured' != s:
                    wwpns.add(i)
            node['WWPN'] = list(wwpns)
            LOG.info('WWPN on node %(node)s: %(wwpn)s.',
                     {'node': node['id'], 'wwpn': node['WWPN']})

    def _get_vdisk_map_properties(
            self, connector, lun_id, vdisk_name, vdisk_id, vdisk_params):
        """Get the map properties of vdisk."""

        LOG.debug(
            'enter: _get_vdisk_map_properties: vdisk '
            '%(vdisk_name)s.', {'vdisk_name': vdisk_name})

        IO_group = '0'

        io_group_nodes = []
        for k, node in self._storage_nodes.items():
            if vdisk_params['protocol'] != node['protocol']:
                continue
            if node['IO_group'] == IO_group:
                io_group_nodes.append(node)

        if not io_group_nodes:
            msg = (_('_get_vdisk_map_properties: No node found in '
                     'I/O group %(gid)s for volume %(vol)s.')
                   % {'gid': IO_group, 'vol': vdisk_name})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)

        properties = {}
        properties['target_discovered'] = False
        properties['target_lun'] = lun_id
        properties['volume_id'] = vdisk_id

        type_str = 'fibre_channel'
        conn_wwpns = self._get_conn_fc_wwpns()

        if not conn_wwpns:
            msg = _('_get_vdisk_map_properties: Could not get FC '
                    'connection information for the host-volume '
                    'connection. Is the host configured properly '
                    'for FC connections?')
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)

        properties['target_wwn'] = conn_wwpns

        if "zvm_fcp" in connector:
            properties['zvm_fcp'] = connector['zvm_fcp']

        properties['initiator_target_map'] = self._build_initiator_target_map(
            connector['wwpns'], conn_wwpns)

        LOG.debug(
            'leave: _get_vdisk_map_properties: vdisk '
            '%(vdisk_name)s.', {'vdisk_name': vdisk_name})

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

    @fczm_utils.add_fc_zone
    @utils.synchronized('flashsystem-init-conn', external=True)
    def initialize_connection(self, volume, connector):
        """Perform work so that an FC connection can be made.

        To be able to create a FC connection from a given host to a
        volume, we must:
        1. Translate the given WWNN to a host name
        2. Create new host on the storage system if it does not yet exist
        3. Map the volume to the host if it is not already done
        4. Return the connection information for relevant nodes (in the
           proper I/O group)

        """

        LOG.debug(
            'enter: initialize_connection: volume %(vol)s with '
            'connector %(conn)s.', {'vol': volume, 'conn': connector})

        vdisk_name = volume['name']
        vdisk_id = volume['id']
        vdisk_params = self._get_vdisk_params(volume['volume_type_id'])

        # TODO(edwin): might fix it after vdisk copy function is
        # ready in FlashSystem thin-provision layer. As this validation
        # is to check the vdisk which is in copying, at present in firmware
        # level vdisk doesn't allow to map host which it is copy. New
        # vdisk clone and snapshot function will cover it. After that the
        # _wait_vdisk_copy_completed need some modification.
        self._wait_vdisk_copy_completed(vdisk_name)

        self._driver_assert(
            self._is_vdisk_defined(vdisk_name),
            (_('initialize_connection: vdisk %s is not defined.')
             % vdisk_name))

        lun_id = self._map_vdisk_to_host(vdisk_name, connector)

        properties = {}
        try:
            properties = self._get_vdisk_map_properties(
                connector, lun_id, vdisk_name, vdisk_id, vdisk_params)
        except exception.VolumeBackendAPIException:
            with excutils.save_and_reraise_exception():
                self.terminate_connection(volume, connector)
                LOG.error('initialize_connection: Failed to collect '
                          'return properties for volume %(vol)s and '
                          'connector %(conn)s.',
                          {'vol': volume, 'conn': connector})

        LOG.debug(
            'leave: initialize_connection:\n volume: %(vol)s\n connector '
            '%(conn)s\n properties: %(prop)s.',
            {'vol': volume,
             'conn': connector,
             'prop': properties})

        return properties

    @fczm_utils.remove_fc_zone
    @utils.synchronized('flashsystem-term-conn', external=True)
    def terminate_connection(self, volume, connector, **kwargs):
        """Cleanup after connection has been terminated.

        When we clean up a terminated connection between a given connector
        and volume, we:
        1. Translate the given connector to a host name
        2. Remove the volume-to-host mapping if it exists
        3. Delete the host if it has no more mappings (hosts are created
           automatically by this driver when mappings are created)
        """
        LOG.debug(
            'enter: terminate_connection: volume %(vol)s with '
            'connector %(conn)s.',
            {'vol': volume, 'conn': connector})

        return_data = {
            'driver_volume_type': 'fibre_channel',
            'data': {},
        }

        vdisk_name = volume['name']
        self._wait_vdisk_copy_completed(vdisk_name)
        self._unmap_vdisk_from_host(vdisk_name, connector)

        host_name = self._get_host_from_connector(connector)
        if not host_name:
            properties = {}
            conn_wwpns = self._get_conn_fc_wwpns()
            properties['target_wwn'] = conn_wwpns
            properties['initiator_target_map'] = (
                self._build_initiator_target_map(
                    connector['wwpns'], conn_wwpns))
            return_data['data'] = properties

        LOG.debug(
            'leave: terminate_connection: volume %(vol)s with '
            'connector %(conn)s.', {'vol': volume, 'conn': connector})

        return return_data

    def do_setup(self, ctxt):
        """Check that we have all configuration details from the storage."""

        self._context = ctxt

        # Get data of configured node
        self._get_node_data()

        # Get the WWPNs of the FlashSystem nodes
        self._get_fc_wwpns()

        # For each node, check what connection modes it supports.  Delete any
        # nodes that do not support any types (may be partially configured).
        to_delete = []
        for k, node in self._storage_nodes.items():
            if not node['WWPN']:
                to_delete.append(k)

        for delkey in to_delete:
            del self._storage_nodes[delkey]

        # Make sure we have at least one node configured
        self._driver_assert(self._storage_nodes,
                            'do_setup: No configured nodes.')

        self._protocol = node['protocol'] = 'FC'

        # Set for vdisk synchronization
        self._vdisk_copy_in_progress = set()
        self._vdisk_copy_lock = threading.Lock()
        self._check_lock_interval = 5

    def validate_connector(self, connector):
        """Check connector."""
        if 'FC' == self._protocol and 'wwpns' not in connector:
            LOG.error('The connector does not contain the '
                      'required information: wwpns is missing')
            raise exception.InvalidConnectorException(missing='wwpns')
