# 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 iSCSI 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

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

LOG = logging.getLogger(__name__)

flashsystem_iscsi_opts = [
    cfg.IntOpt('flashsystem_iscsi_portid',
               default=0,
               help='Default iSCSI Port ID of FlashSystem. '
                    '(Default port is 0.)')
]

CONF = cfg.CONF
CONF.register_opts(flashsystem_iscsi_opts, group=conf.SHARED_CONF_GROUP)


@interface.volumedriver
class FlashSystemISCSIDriver(fscommon.FlashSystemDriver):
    """IBM FlashSystem iSCSI 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(FlashSystemISCSIDriver, self).__init__(*args, **kwargs)
        self.configuration.append_config_values(fscommon.flashsystem_opts)
        self.configuration.append_config_values(flashsystem_iscsi_opts)
        self.configuration.append_config_values(san.san_opts)

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

        # Check if iscsi_ip is set when protocol is iSCSI
        if params['protocol'] == 'iSCSI' and params['iscsi_ip'] == 'None':
            msg = _("target_ip_address must be set in config file when "
                    "using protocol 'iSCSI'.")
            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 = str(random.randint(0, 99999999)).zfill(8)
        host_name = '%s-%s' % (self._connector_to_hostname_prefix(connector),
                               rand_id)

        ports = []

        if 'iSCSI' == self._protocol and 'initiator' in connector:
            ports.append('-iscsiname %s' % connector['initiator'])

        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):
        LOG.debug('enter: _find_host_exhaustive hosts: %s.', 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)
            for attr_line in out.split('\n'):
                attr_name, foo, attr_val = attr_line.partition('!')
                if (attr_name == 'iscsi_name' and
                        'initiator' in connector and
                        attr_val == connector['initiator']):
                    LOG.debug(
                        'leave: _find_host_exhaustive connector: %s.',
                        connector)
                    return host
        else:
            LOG.warning('Host %(host)s was not found on backend storage.',
                        {'host': hname})
        return None

    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})

        preferred_node = '0'
        IO_group = '0'

        # Get preferred node and other nodes in I/O group
        preferred_node_entry = None
        io_group_nodes = []
        for k, node in self._storage_nodes.items():
            if vdisk_params['protocol'] != node['protocol']:
                continue
            if node['id'] == preferred_node:
                preferred_node_entry = node
            if node['IO_group'] == IO_group:
                io_group_nodes.append(node)

        if not io_group_nodes:
            msg = (_('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)

        if not preferred_node_entry:
            # Get 1st node in I/O group
            preferred_node_entry = io_group_nodes[0]
            LOG.warning('_get_vdisk_map_properties: Did not find a '
                        'preferred node for vdisk %s.', vdisk_name)
        properties = {
            'target_discovered': False,
            'target_lun': lun_id,
            'volume_id': vdisk_id,
        }

        type_str = 'iscsi'
        if preferred_node_entry['ipv4']:
            ipaddr = preferred_node_entry['ipv4'][0]
        else:
            ipaddr = preferred_node_entry['ipv6'][0]
        iscsi_port = self.configuration.target_port
        properties['target_portal'] = '%s:%s' % (ipaddr, iscsi_port)
        properties['target_iqn'] = preferred_node_entry['iscsi_name']

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

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

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

        To be able to create an iSCSI connection from a given host to a
        volume, we must:
        1. Translate the given iSCSI name 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'])

        self._wait_vdisk_copy_completed(vdisk_name)

        self._driver_assert(
            self._is_vdisk_defined(vdisk_name),
            (_('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('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

    @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})

        vdisk_name = volume['name']
        self._wait_vdisk_copy_completed(vdisk_name)
        host_name = self._unmap_vdisk_from_host(vdisk_name, connector)
        # checking if host_name none, if not then, check if the host has
        # any mappings, if not the host gets deleted.
        if host_name:
            if not self._get_hostvdisk_mappings(host_name):
                self._delete_host(host_name)

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

        return {'driver_volume_type': 'iscsi'}

    def _get_iscsi_ip_addrs(self):
        """get ip address of iSCSI interface."""

        LOG.debug('enter: _get_iscsi_ip_addrs')

        cmd = ['svcinfo', 'lsportip']
        generator = self._port_conf_generator(cmd)
        header = next(generator, None)
        if not header:
            return

        for key in self._storage_nodes:
            if self._storage_nodes[key]['config_node'] == 'yes':
                node = self._storage_nodes[key]
                break

        if node is None:
            msg = _('No config node found.')
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        for port_data in generator:
            try:
                port_ipv4 = port_data['IP_address']
                port_ipv6 = port_data['IP_address_6']
                state = port_data['state']
                speed = port_data['speed']
            except KeyError:
                self._handle_keyerror('lsportip', header)
            if port_ipv4 == self.configuration.target_ip_address and (
                    port_data['id'] == (
                        str(self.configuration.flashsystem_iscsi_portid))):
                if state not in ('configured', 'online'):
                    msg = (_('State of node is wrong. Current state is %s.')
                           % state)
                    LOG.error(msg)
                    raise exception.VolumeBackendAPIException(data=msg)
                if state in ('configured', 'online') and speed != 'NONE':
                    if port_ipv4:
                        node['ipv4'].append(port_ipv4)
                    if port_ipv6:
                        node['ipv6'].append(port_ipv6)
                    break
        if not (len(node['ipv4']) or len(node['ipv6'])):
            msg = _('No ip address found.')
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)

        LOG.debug('leave: _get_iscsi_ip_addrs')

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

        LOG.debug('enter: do_setup')

        self._context = ctxt

        # Get data of configured node
        self._get_node_data()

        # Get the iSCSI IP addresses of the FlashSystem nodes
        self._get_iscsi_ip_addrs()

        for k, node in self._storage_nodes.items():
            if self.configuration.flashsystem_connection_protocol == 'iSCSI':
                if (len(node['ipv4']) or len(node['ipv6']) and
                        len(node['iscsi_name'])):
                    node['protocol'] = 'iSCSI'

        self._protocol = 'iSCSI'

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

        LOG.debug('leave: do_setup')

    def _build_default_params(self):
        protocol = self.configuration.flashsystem_connection_protocol
        if protocol.lower() == 'iscsi':
            protocol = 'iSCSI'
        return {
            'protocol': protocol,
            'iscsi_ip': self.configuration.target_ip_address,
            'iscsi_port': self.configuration.target_port,
            'iscsi_ported': self.configuration.flashsystem_iscsi_portid,
        }

    def validate_connector(self, connector):
        """Check connector for enabled protocol."""
        valid = False
        if 'iSCSI' == self._protocol and 'initiator' in connector:
            valid = True
        if not valid:
            LOG.error('The connector does not contain the '
                      'required information: initiator is missing')
            raise exception.InvalidConnectorException(missing=(
                                                      'initiator'))
