# Copyright 2015 Violin Memory, 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.

"""
Violin 7000 Series All-Flash Array Volume Driver

Provides fibre channel specific LUN services for V7000 series flash
arrays.

This driver requires Concerto v7.0.0 or newer software on the array.

You will need to install the Violin Memory REST client library:
sudo pip install vmemclient

Set the following in the cinder.conf file to enable the VMEM V7000
Fibre Channel Driver along with the required flags:

volume_driver=cinder.volume.drivers.violin.v7000_fcp.V7000FCDriver

NOTE: this driver file requires the use of synchronization points for
certain types of backend operations, and as a result may not work
properly in an active-active HA configuration.  See OpenStack Cinder
driver documentation for more information.
"""

from oslo_log import log as logging

from cinder import exception
from cinder.i18n import _, _LE, _LI
from cinder import interface
from cinder import utils
from cinder.volume import driver
from cinder.volume.drivers.san import san
from cinder.volume.drivers.violin import v7000_common
from cinder.zonemanager import utils as fczm_utils

import socket

LOG = logging.getLogger(__name__)


@interface.volumedriver
class V7000FCPDriver(driver.FibreChannelDriver):
    """Executes commands relating to fibre channel based Violin Memory arrays.

    Version history:
        1.0 - Initial driver
    """

    VERSION = '1.0'

    # ThirdPartySystems wiki page
    CI_WIKI_NAME = "Violin_Memory_CI"

    def __init__(self, *args, **kwargs):
        super(V7000FCPDriver, self).__init__(*args, **kwargs)
        self.gateway_fc_wwns = []
        self.stats = {}
        self.configuration.append_config_values(v7000_common.violin_opts)
        self.configuration.append_config_values(san.san_opts)
        self.common = v7000_common.V7000Common(self.configuration)
        self.lookup_service = fczm_utils.create_lookup_service()

        LOG.info(_LI("Initialized driver %(name)s version: %(vers)s"),
                 {'name': self.__class__.__name__, 'vers': self.VERSION})

    def do_setup(self, context):
        """Any initialization the driver does while starting."""
        super(V7000FCPDriver, self).do_setup(context)

        self.common.do_setup(context)
        self.gateway_fc_wwns = self._get_active_fc_targets()

        # Register the client with the storage array
        fc_version = self.VERSION + "-FCP"
        self.common.vmem_mg.utility.set_managed_by_openstack_version(
            fc_version)

    def check_for_setup_error(self):
        """Returns an error if prerequisites aren't met."""
        self.common.check_for_setup_error()
        if len(self.gateway_fc_wwns) == 0:
            raise exception.ViolinInvalidBackendConfig(
                reason=_('No FCP targets found'))

    def create_volume(self, volume):
        """Creates a volume."""
        self.common._create_lun(volume)

    def create_volume_from_snapshot(self, volume, snapshot):
        """Creates a volume from a snapshot."""
        self.common._create_volume_from_snapshot(snapshot, volume)

    def create_cloned_volume(self, volume, src_vref):
        """Creates a clone of the specified volume."""
        self.common._create_lun_from_lun(src_vref, volume)

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

    def extend_volume(self, volume, new_size):
        """Extend an existing volume's size."""
        self.common._extend_lun(volume, new_size)

    def create_snapshot(self, snapshot):
        """Creates a snapshot."""
        self.common._create_lun_snapshot(snapshot)

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

    def ensure_export(self, context, volume):
        """Synchronously checks and re-exports volumes at cinder start time."""
        pass

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

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

    @fczm_utils.AddFCZone
    def initialize_connection(self, volume, connector):
        """Allow connection to connector and return connection info."""

        LOG.debug("Initialize_connection: initiator - %(initiator)s  host - "
                  "%(host)s wwpns - %(wwpns)s",
                  {'initiator': connector['initiator'],
                   'host': connector['host'],
                   'wwpns': connector['wwpns']})

        self.common.vmem_mg.client.create_client(
            name=connector['host'], proto='FC', fc_wwns=connector['wwpns'])

        lun_id = self._export_lun(volume, connector)

        target_wwns, init_targ_map = self._build_initiator_target_map(
            connector)

        properties = {}
        properties['target_discovered'] = True
        properties['target_wwn'] = target_wwns
        properties['target_lun'] = lun_id
        properties['initiator_target_map'] = init_targ_map

        LOG.debug("Return FC data for zone addition: %(properties)s.",
                  {'properties': properties})

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

    @fczm_utils.RemoveFCZone
    def terminate_connection(self, volume, connector, **kwargs):
        """Terminates the connection (target<-->initiator)."""

        self._unexport_lun(volume, connector)

        properties = {}

        if not self._is_initiator_connected_to_array(connector):
            target_wwns, init_targ_map = self._build_initiator_target_map(
                connector)
            properties['target_wwn'] = target_wwns
            properties['initiator_target_map'] = init_targ_map

        LOG.debug("Return FC data for zone deletion: %(properties)s.",
                  {'properties': properties})

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

    def get_volume_stats(self, refresh=False):
        """Get volume stats.

        If 'refresh' is True, update the stats first.
        """
        if refresh or not self.stats:
            self._update_volume_stats()
        return self.stats

    @utils.synchronized('vmem-export')
    def _export_lun(self, volume, connector=None):
        """Generates the export configuration for the given volume.

        :param volume:  volume object provided by the Manager
        :param connector:  connector object provided by the Manager
        :returns: the LUN ID assigned by the backend
        """
        lun_id = ''
        v = self.common.vmem_mg

        if not connector:
            raise exception.ViolinInvalidBackendConfig(
                reason=_('No initiators found, cannot proceed'))

        LOG.debug("Exporting lun %(vol_id)s - initiator wwpns %(i_wwpns)s "
                  "- target wwpns %(t_wwpns)s.",
                  {'vol_id': volume['id'], 'i_wwpns': connector['wwpns'],
                   't_wwpns': self.gateway_fc_wwns})

        try:
            lun_id = self.common._send_cmd_and_verify(
                v.lun.assign_lun_to_client,
                self._is_lun_id_ready,
                "Assign SAN client successfully",
                [volume['id'], connector['host'],
                 "ReadWrite"],
                [volume['id'], connector['host']])

        except exception.ViolinBackendErr:
            LOG.exception(_LE("Backend returned err for lun export."))
            raise

        except Exception:
            raise exception.ViolinInvalidBackendConfig(
                reason=_('LUN export failed!'))

        lun_id = self._get_lun_id(volume['id'], connector['host'])
        LOG.info(_LI("Exported lun %(vol_id)s on lun_id %(lun_id)s."),
                 {'vol_id': volume['id'], 'lun_id': lun_id})

        return lun_id

    @utils.synchronized('vmem-export')
    def _unexport_lun(self, volume, connector=None):
        """Removes the export configuration for the given volume.

        :param volume:  volume object provided by the Manager
        """
        v = self.common.vmem_mg

        LOG.info(_LI("Unexporting lun %s."), volume['id'])

        try:
            self.common._send_cmd(v.lun.unassign_client_lun,
                                  "Unassign SAN client successfully",
                                  volume['id'], connector['host'], True)

        except exception.ViolinBackendErr:
            LOG.exception(_LE("Backend returned err for lun export."))
            raise

        except Exception:
            LOG.exception(_LE("LUN unexport failed!"))
            raise

    def _update_volume_stats(self):
        """Gathers array stats and converts them to GB values."""
        data = {}
        total_gb = 0
        free_gb = 0
        v = self.common.vmem_mg.basic
        array_name_triple = socket.gethostbyaddr(self.configuration.san_ip)
        array_name = array_name_triple[0]

        phy_devices = v.get("/batch/physicalresource/physicaldevice")

        all_devices = [x for x in phy_devices['data']['physical_devices']]

        for x in all_devices:
            if socket.getfqdn(x['owner']) == array_name:
                total_gb += x['size_mb'] // 1024
                free_gb += x['availsize_mb'] // 1024

        backend_name = self.configuration.volume_backend_name
        data['volume_backend_name'] = backend_name or self.__class__.__name__
        data['vendor_name'] = 'Violin Memory, Inc.'
        data['driver_version'] = self.VERSION
        data['storage_protocol'] = 'fibre_channel'
        data['reserved_percentage'] = 0
        data['QoS_support'] = False
        data['total_capacity_gb'] = total_gb
        data['free_capacity_gb'] = free_gb
        for i in data:
            LOG.debug("stat update: %(name)s=%(data)s",
                      {'name': i, 'data': data[i]})

        self.stats = data

    def _get_active_fc_targets(self):
        """Get a list of gateway WWNs that can be used as FCP targets.

        :param mg_conn:  active XG connection to one of the gateways
        :returns:  list of WWNs in openstack format
        """
        v = self.common.vmem_mg
        active_gw_fcp_wwns = []

        fc_info = v.adapter.get_fc_info()
        for x in fc_info.values():
            active_gw_fcp_wwns.append(x[0])

        return active_gw_fcp_wwns

    def _get_lun_id(self, volume_name, client_name):
        """Get the lun ID for an exported volume.

        If the lun is successfully assigned (exported) to a client, the
        client info has the lun_id.

        :param volume_name:  name of volume to query for lun ID
        :param client_name:  name of client associated with the volume
        :returns: integer value of lun ID
        """
        v = self.common.vmem_mg
        lun_id = None

        client_info = v.client.get_client_info(client_name)

        for x in client_info['FibreChannelDevices']:
            if volume_name == x['name']:
                lun_id = x['lun']
                break

        if lun_id:
            lun_id = int(lun_id)

        return lun_id

    def _is_lun_id_ready(self, volume_name, client_name):
        """Get the lun ID for an exported volume.

        If the lun is successfully assigned (exported) to a client, the
        client info has the lun_id.

        :param volume_name:  name of volume to query for lun ID
        :param client_name:  name of client associated with the volume
        :returns: Returns True if lun is ready, False otherwise
        """

        lun_id = -1
        lun_id = self._get_lun_id(volume_name, client_name)
        if lun_id is None:
            return False
        else:
            return True

    def _build_initiator_target_map(self, connector):
        """Build the target_wwns and the initiator target map."""
        target_wwns = []
        init_targ_map = {}

        if self.lookup_service:
            dev_map = self.lookup_service.get_device_mapping_from_network(
                connector['wwpns'], self.gateway_fc_wwns)

            for fabric_name in dev_map:
                fabric = dev_map[fabric_name]
                target_wwns += fabric['target_port_wwn_list']
                for initiator in fabric['initiator_port_wwn_list']:
                    if initiator not in init_targ_map:
                        init_targ_map[initiator] = []
                    init_targ_map[initiator] += fabric['target_port_wwn_list']
                    init_targ_map[initiator] = list(
                        set(init_targ_map[initiator]))

            target_wwns = list(set(target_wwns))

        else:
            initiator_wwns = connector['wwpns']
            target_wwns = self.gateway_fc_wwns
            for initiator in initiator_wwns:
                init_targ_map[initiator] = target_wwns

        return target_wwns, init_targ_map

    def _is_initiator_connected_to_array(self, connector):
        """Check if any initiator wwns still have active sessions."""
        v = self.common.vmem_mg

        client = v.client.get_client_info(connector['host'])

        if len(client['FibreChannelDevices']):
            # each entry in the FibreChannelDevices array is a dict
            # describing an active lun assignment
            return True
        return False
