# Copyright (c) 2018 Dell Inc. or its subsidiaries.
# 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.
"""Driver for Dell EMC XtremIO Storage.

Supports XtremIO version 2.4 and up.

.. code-block:: none

  1.0.0 - initial release
  1.0.1 - enable volume extend
  1.0.2 - added FC support, improved error handling
  1.0.3 - update logging level, add translation
  1.0.4 - support for FC zones
  1.0.5 - add support for XtremIO 4.0
  1.0.6 - add support for iSCSI multipath, CA validation, consistency groups,
          R/O snapshots, CHAP discovery authentication
  1.0.7 - cache glance images on the array
  1.0.8 - support for volume retype, CG fixes
  1.0.9 - performance improvements, support force detach, support for X2
  1.0.10 - option to clean unused IGs
  1.0.11 - add support for multiattach
  1.0.12 - add support for ports filtering
  1.0.13 - add support for iSCSI IPv6
"""

import http.client as http_client
import json
import math
import random
import string

from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import netutils
from oslo_utils import strutils
from oslo_utils import units
import requests

from cinder.common import constants
from cinder import context
from cinder import exception
from cinder.i18n import _
from cinder import interface
from cinder.objects import fields
from cinder import utils
from cinder.volume import configuration
from cinder.volume import driver
from cinder.volume.drivers.san import san
from cinder.volume import volume_utils
from cinder.zonemanager import utils as fczm_utils

LOG = logging.getLogger(__name__)

CONF = cfg.CONF
XTREMIO_OPTS = [
    cfg.StrOpt('xtremio_cluster_name',
               default='',
               help='XMS cluster id in multi-cluster environment'),
    cfg.IntOpt('xtremio_array_busy_retry_count',
               default=5,
               help='Number of retries in case array is busy'),
    cfg.IntOpt('xtremio_array_busy_retry_interval',
               default=5,
               help='Interval between retries in case array is busy'),
    cfg.IntOpt('xtremio_volumes_per_glance_cache',
               default=100,
               help='Number of volumes created from each cached glance image'),
    cfg.BoolOpt('xtremio_clean_unused_ig',
                default=False,
                help='Should the driver remove initiator groups with no '
                     'volumes after the last connection was terminated. '
                     'Since the behavior till now was to leave '
                     'the IG be, we default to False (not deleting IGs '
                     'without connected volumes); setting this parameter '
                     'to True will remove any IG after terminating its '
                     'connection to the last volume.'),
    cfg.ListOpt('xtremio_ports',
                default=[],
                help='Allowed ports. Comma separated list of XtremIO '
                     'iSCSI IPs or FC WWNs (ex. 58:cc:f0:98:49:22:07:02) '
                     'to be used. If option is not set all ports are allowed.')
]

CONF.register_opts(XTREMIO_OPTS, group=configuration.SHARED_CONF_GROUP)

RANDOM = random.Random()
OBJ_NOT_FOUND_ERR = 'obj_not_found'
VOL_NOT_UNIQUE_ERR = 'vol_obj_name_not_unique'
VOL_OBJ_NOT_FOUND_ERR = 'vol_obj_not_found'
ALREADY_MAPPED_ERR = 'already_mapped'
SYSTEM_BUSY = 'system_is_busy'
TOO_MANY_OBJECTS = 'too_many_objs'
TOO_MANY_SNAPSHOTS_PER_VOL = 'too_many_snapshots_per_vol'


XTREMIO_OID_NAME = 1
XTREMIO_OID_INDEX = 2


class XtremIOAlreadyMappedError(exception.VolumeDriverException):
    message = _("Volume to Initiator Group mapping already exists")


class XtremIOArrayBusy(exception.VolumeDriverException):
    message = _("System is busy, retry operation.")


class XtremIOSnapshotsLimitExceeded(exception.VolumeDriverException):
    message = _("Exceeded the limit of snapshots per volume")


class XtremIOClient(object):
    def __init__(self, configuration, cluster_id):
        self.configuration = configuration
        self.cluster_id = cluster_id
        self.verify = (self.configuration.
                       safe_get('driver_ssl_cert_verify') or False)
        if self.verify:
            verify_path = (self.configuration.
                           safe_get('driver_ssl_cert_path') or None)
            if verify_path:
                self.verify = verify_path

    def get_base_url(self, ver):
        if ver == 'v1':
            return 'https://%s/api/json/types' % self.configuration.san_ip
        elif ver == 'v2':
            return 'https://%s/api/json/v2/types' % self.configuration.san_ip

    def req(self, object_type='volumes', method='GET', data=None,
            name=None, idx=None, ver='v1'):
        @utils.retry(XtremIOArrayBusy,
                     self.configuration.xtremio_array_busy_retry_count,
                     self.configuration.xtremio_array_busy_retry_interval, 1)
        def _do_req(object_type, method, data, name, idx, ver):
            if not data:
                data = {}
            if name and idx:
                msg = _("can't handle both name and index in req")
                LOG.error(msg)
                raise exception.VolumeDriverException(message=msg)

            url = '%s/%s' % (self.get_base_url(ver), object_type)
            params = {}
            key = None
            if name:
                params['name'] = name
                key = name
            elif idx:
                url = '%s/%d' % (url, idx)
                key = str(idx)
            if method in ('GET', 'DELETE'):
                params.update(data)
                self.update_url(params, self.cluster_id)
            if method != 'GET':
                self.update_data(data, self.cluster_id)
                # data may include chap password
                LOG.debug('data: %s', strutils.mask_password(data))
            LOG.debug('%(type)s %(url)s', {'type': method, 'url': url})
            try:
                response = requests.request(
                    method, url, params=params, data=json.dumps(data),
                    verify=self.verify, auth=(self.configuration.san_login,
                                              self.configuration.san_password))
            except requests.exceptions.RequestException as exc:
                msg = (_('Exception: %s') % str(exc))
                raise exception.VolumeDriverException(message=msg)

            if (http_client.OK <= response.status_code <
                    http_client.MULTIPLE_CHOICES):
                if method in ('GET', 'POST'):
                    return response.json()
                else:
                    return ''

            self.handle_errors(response, key, object_type)
        return _do_req(object_type, method, data, name, idx, ver)

    def handle_errors(self, response, key, object_type):
        if response.status_code == http_client.BAD_REQUEST:
            error = response.json()
            err_msg = error.get('message')
            if err_msg.endswith(OBJ_NOT_FOUND_ERR):
                LOG.warning("object %(key)s of "
                            "type %(typ)s not found, %(err_msg)s",
                            {'key': key, 'typ': object_type,
                             'err_msg': err_msg, })
                raise exception.NotFound()
            elif err_msg == VOL_NOT_UNIQUE_ERR:
                LOG.error("can't create 2 volumes with the same name, %s",
                          err_msg)
                msg = _('Volume by this name already exists')
                raise exception.VolumeBackendAPIException(data=msg)
            elif err_msg == VOL_OBJ_NOT_FOUND_ERR:
                LOG.error("Can't find volume to map %(key)s, %(msg)s",
                          {'key': key, 'msg': err_msg, })
                raise exception.VolumeNotFound(volume_id=key)
            elif ALREADY_MAPPED_ERR in err_msg:
                raise XtremIOAlreadyMappedError()
            elif err_msg == SYSTEM_BUSY:
                raise XtremIOArrayBusy()
            elif err_msg in (TOO_MANY_OBJECTS, TOO_MANY_SNAPSHOTS_PER_VOL):
                raise XtremIOSnapshotsLimitExceeded()
        msg = _('Bad response from XMS, %s') % response.text
        LOG.error(msg)
        raise exception.VolumeBackendAPIException(message=msg)

    def update_url(self, data, cluster_id):
        return

    def update_data(self, data, cluster_id):
        return

    def get_cluster(self):
        return self.req('clusters', idx=1)['content']

    def create_snapshot(self, src, dest, ro=False):
        """Create a snapshot of a volume on the array.

        XtreamIO array snapshots are also volumes.

        :src: name of the source volume to be cloned
        :dest: name for the new snapshot
        :ro: new snapshot type ro/regular. only applicable to Client4
        """
        raise NotImplementedError()

    def get_extra_capabilities(self):
        return {}

    def get_initiator(self, port_address):
        raise NotImplementedError()

    def add_vol_to_cg(self, vol_id, cg_id):
        pass

    def get_initiators_igs(self, port_addresses):
        ig_indexes = set()
        for port_address in port_addresses:
            initiator = self.get_initiator(port_address)
            ig_indexes.add(initiator['ig-id'][XTREMIO_OID_INDEX])

        return list(ig_indexes)

    def get_fc_up_ports(self):
        targets = [self.req('targets', name=target['name'])['content']
                   for target in self.req('targets')['targets']]
        return [target for target in targets
                if target['port-type'] == 'fc' and
                target["port-state"] == 'up']


class XtremIOClient3(XtremIOClient):
    def __init__(self, configuration, cluster_id):
        super(XtremIOClient3, self).__init__(configuration, cluster_id)
        self._portals = []

    def find_lunmap(self, ig_name, vol_name):
        try:
            lun_mappings = self.req('lun-maps')['lun-maps']
        except exception.NotFound:
            raise (exception.VolumeDriverException
                   (_("can't find lun-map, ig:%(ig)s vol:%(vol)s") %
                    {'ig': ig_name, 'vol': vol_name}))

        for lm_link in lun_mappings:
            idx = lm_link['href'].split('/')[-1]
            # NOTE(geguileo): There can be races so mapped elements retrieved
            # in the listing may no longer exist.
            try:
                lm = self.req('lun-maps', idx=int(idx))['content']
            except exception.NotFound:
                continue
            if lm['ig-name'] == ig_name and lm['vol-name'] == vol_name:
                return lm

        return None

    def num_of_mapped_volumes(self, initiator):
        cnt = 0
        for lm_link in self.req('lun-maps')['lun-maps']:
            idx = lm_link['href'].split('/')[-1]
            # NOTE(geguileo): There can be races so mapped elements retrieved
            # in the listing may no longer exist.
            try:
                lm = self.req('lun-maps', idx=int(idx))['content']
            except exception.NotFound:
                continue
            if lm['ig-name'] == initiator:
                cnt += 1
        return cnt

    def get_iscsi_portals(self):
        if self._portals:
            return self._portals

        iscsi_portals = [t['name'] for t in self.req('iscsi-portals')
                         ['iscsi-portals']]
        for portal_name in iscsi_portals:
            try:
                self._portals.append(self.req('iscsi-portals',
                                              name=portal_name)['content'])
            except exception.NotFound:
                raise (exception.VolumeBackendAPIException
                       (data=_("iscsi portal, %s, not found") % portal_name))

        return self._portals

    def create_snapshot(self, src, dest, ro=False):
        data = {'snap-vol-name': dest, 'ancestor-vol-id': src}

        self.req('snapshots', 'POST', data)

    def get_initiator(self, port_address):
        try:
            return self.req('initiators', 'GET', name=port_address)['content']
        except exception.NotFound:
            pass


class XtremIOClient4(XtremIOClient):
    def __init__(self, configuration, cluster_id):
        super(XtremIOClient4, self).__init__(configuration, cluster_id)
        self._cluster_name = None

    def req(self, object_type='volumes', method='GET', data=None,
            name=None, idx=None, ver='v2'):
        return super(XtremIOClient4, self).req(object_type, method, data,
                                               name, idx, ver)

    def get_extra_capabilities(self):
        return {'consistencygroup_support': True}

    def find_lunmap(self, ig_name, vol_name):
        try:
            return (self.req('lun-maps',
                             data={'full': 1,
                                   'filter': ['vol-name:eq:%s' % vol_name,
                                              'ig-name:eq:%s' % ig_name]})
                    ['lun-maps'][0])
        except (KeyError, IndexError):
            raise exception.VolumeNotFound(volume_id=vol_name)

    def num_of_mapped_volumes(self, initiator):
        return len(self.req('lun-maps',
                            data={'filter': 'ig-name:eq:%s' % initiator})
                   ['lun-maps'])

    def update_url(self, data, cluster_id):
        if cluster_id:
            data['cluster-name'] = cluster_id

    def update_data(self, data, cluster_id):
        if cluster_id:
            data['cluster-id'] = cluster_id

    def get_iscsi_portals(self):
        return self.req('iscsi-portals',
                        data={'full': 1})['iscsi-portals']

    def get_cluster(self):
        if not self.cluster_id:
            self.cluster_id = self.req('clusters')['clusters'][0]['name']

        return self.req('clusters', name=self.cluster_id)['content']

    def create_snapshot(self, src, dest, ro=False):
        data = {'snapshot-set-name': dest, 'snap-suffix': dest,
                'volume-list': [src],
                'snapshot-type': 'readonly' if ro else 'regular'}

        res = self.req('snapshots', 'POST', data, ver='v2')
        typ, idx = res['links'][0]['href'].split('/')[-2:]

        # rename the snapshot
        data = {'name': dest}
        try:
            self.req(typ, 'PUT', data, idx=int(idx))
        except exception.VolumeBackendAPIException:
            # reverting
            LOG.error('Failed to rename the created snapshot, reverting.')
            self.req(typ, 'DELETE', idx=int(idx))
            raise

    def add_vol_to_cg(self, vol_id, cg_id):
        add_data = {'vol-id': vol_id, 'cg-id': cg_id}
        self.req('consistency-group-volumes', 'POST', add_data, ver='v2')

    def get_initiator(self, port_address):
        inits = self.req('initiators',
                         data={'filter': 'port-address:eq:' + port_address,
                               'full': 1})['initiators']
        if len(inits) == 1:
            return inits[0]
        else:
            pass

    def get_fc_up_ports(self):
        return self.req('targets',
                        data={'full': 1,
                              'filter': ['port-type:eq:fc',
                                         'port-state:eq:up'],
                              'prop': 'port-address'})["targets"]


class XtremIOClient42(XtremIOClient4):
    def get_initiators_igs(self, port_addresses):
        init_filter = ','.join('port-address:eq:{}'.format(port_address) for
                               port_address in port_addresses)
        initiators = self.req('initiators',
                              data={'filter': init_filter,
                                    'full': 1, 'prop': 'ig-id'})['initiators']
        return list(set(ig_id['ig-id'][XTREMIO_OID_INDEX]
                        for ig_id in initiators))


class XtremIOVolumeDriver(san.SanDriver):
    """Executes commands relating to Volumes."""

    VERSION = '1.0.13'

    # ThirdPartySystems wiki
    CI_WIKI_NAME = "DellEMC_XtremIO_CI"

    driver_name = 'XtremIO'
    MIN_XMS_VERSION = [3, 0, 0]

    SUPPORTED = False

    def __init__(self, *args, **kwargs):
        super(XtremIOVolumeDriver, self).__init__(*args, **kwargs)
        self.configuration.append_config_values(XTREMIO_OPTS)
        self.protocol = None
        self.backend_name = (self.configuration.safe_get('volume_backend_name')
                             or self.driver_name)
        self.cluster_id = (self.configuration.safe_get('xtremio_cluster_name')
                           or '')
        self.provisioning_factor = \
            volume_utils.get_max_over_subscription_ratio(
                self.configuration.max_over_subscription_ratio,
                supports_auto=False)

        self.clean_ig = (self.configuration.safe_get('xtremio_clean_unused_ig')
                         or False)
        self._stats = {}
        self.allowed_ports = [
            port.strip().lower() for port in
            self.configuration.safe_get('xtremio_ports')
        ]
        self.client = XtremIOClient3(self.configuration, self.cluster_id)

    @classmethod
    def get_driver_options(cls):
        additional_opts = cls._get_oslo_driver_opts(
            'san_ip', 'san_login', 'san_password', 'driver_ssl_cert_verify',
            'driver_ssl_cert_path', 'max_over_subscription_ratio',
            'reserved_percentage')
        return XTREMIO_OPTS + additional_opts

    def _obj_from_result(self, res):
        typ, idx = res['links'][0]['href'].split('/')[-2:]
        return self.client.req(typ, idx=int(idx))['content']

    def check_for_setup_error(self):
        try:
            name = self.client.req('clusters')['clusters'][0]['name']
            cluster = self.client.req('clusters', name=name)['content']
            version_text = cluster['sys-sw-version']
        except exception.NotFound:
            msg = _("XtremIO not initialized correctly, no clusters found")
            raise (exception.VolumeBackendAPIException
                   (data=msg))
        ver = [int(n) for n in version_text.split('-')[0].split('.')]
        if ver < self.MIN_XMS_VERSION:
            msg = (_('Invalid XtremIO version %(cur)s,'
                     ' version %(min)s or up is required') %
                   {'min': self.MIN_XMS_VERSION,
                    'cur': ver})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        else:
            LOG.info('XtremIO Cluster version %s', version_text)
        client_ver = '3'
        if ver[0] >= 4:
            # get XMS version
            xms = self.client.req('xms', idx=1)['content']
            xms_version = tuple([int(i) for i in
                                 xms['sw-version'].split('-')[0].split('.')])
            LOG.info('XtremIO XMS version %s', version_text)
            if xms_version >= (4, 2):
                self.client = XtremIOClient42(self.configuration,
                                              self.cluster_id)
                client_ver = '4.2'
            else:
                self.client = XtremIOClient4(self.configuration,
                                             self.cluster_id)
                client_ver = '4'
        LOG.info('Using XtremIO Client %s', client_ver)

    def create_volume(self, volume):
        """Creates a volume."""
        data = {'vol-name': volume['id'],
                'vol-size': str(volume['size']) + 'g'
                }
        self.client.req('volumes', 'POST', data)

        # Add the volume to a cg in case volume requested a cgid or group_id.
        # If both cg_id and group_id exists in a volume. group_id will take
        # place.

        consistency_group = volume.get('consistencygroup_id')

        # if cg_id and group_id are both exists, we gives priority to group_id.
        if volume.get('group_id'):
            consistency_group = volume.get('group_id')

        if consistency_group:
            self.client.add_vol_to_cg(volume['id'],
                                      consistency_group)

    def create_volume_from_snapshot(self, volume, snapshot):
        """Creates a volume from a snapshot."""
        if snapshot.get('cgsnapshot_id'):
            # get array snapshot id from CG snapshot
            snap_by_anc = self._get_snapset_ancestors(snapshot.cgsnapshot)
            snapshot_id = snap_by_anc[snapshot['volume_id']]
        else:
            snapshot_id = snapshot['id']

        try:
            self.client.create_snapshot(snapshot_id, volume['id'])
        except XtremIOSnapshotsLimitExceeded as e:
            raise exception.CinderException(e.message)

        # extend the snapped volume if requested size is larger then original
        if volume['size'] > snapshot['volume_size']:
            try:
                self.extend_volume(volume, volume['size'])
            except Exception:
                LOG.error('failed to extend volume %s, '
                          'reverting volume from snapshot operation',
                          volume['id'])
                # remove the volume in case resize failed
                self.delete_volume(volume)
                raise

        # add new volume to consistency group
        if (volume.get('consistencygroup_id') and
                self.client is XtremIOClient4):
            self.client.add_vol_to_cg(volume['id'],
                                      snapshot['consistencygroup_id'])

    def create_cloned_volume(self, volume, src_vref):
        """Creates a clone of the specified volume."""
        vol = self.client.req('volumes', name=src_vref['id'])['content']
        ctxt = context.get_admin_context()
        cache = self.db.image_volume_cache_get_by_volume_id(ctxt,
                                                            src_vref['id'])
        limit = self.configuration.safe_get('xtremio_volumes_per_glance_cache')
        if cache and limit and limit > 0 and limit <= vol['num-of-dest-snaps']:
            raise exception.SnapshotLimitReached(set_limit=limit)
        try:
            self.client.create_snapshot(src_vref['id'], volume['id'])
        except XtremIOSnapshotsLimitExceeded as e:
            raise exception.CinderException(e.message)

        # extend the snapped volume if requested size is larger then original
        if volume['size'] > src_vref['size']:
            try:
                self.extend_volume(volume, volume['size'])
            except Exception:
                LOG.error('failed to extend volume %s, '
                          'reverting clone operation', volume['id'])
                # remove the volume in case resize failed
                self.delete_volume(volume)
                raise

        if volume.get('consistencygroup_id') and self.client is XtremIOClient4:
            self.client.add_vol_to_cg(volume['id'],
                                      volume['consistencygroup_id'])

    def delete_volume(self, volume):
        """Deletes a volume."""
        try:
            self.client.req('volumes', 'DELETE', name=volume.name_id)
        except exception.NotFound:
            LOG.info("volume %s doesn't exist", volume.name_id)

    def create_snapshot(self, snapshot):
        """Creates a snapshot."""
        self.client.create_snapshot(snapshot.volume_id, snapshot.id, True)

    def delete_snapshot(self, snapshot):
        """Deletes a snapshot."""
        try:
            self.client.req('volumes', 'DELETE', name=snapshot.id)
        except exception.NotFound:
            LOG.info("snapshot %s doesn't exist", snapshot.id)

    def update_migrated_volume(self, ctxt, volume, new_volume,
                               original_volume_status):
        # as the volume name is used to id the volume we need to rename it
        name_id = None
        provider_location = None
        current_name = new_volume['id']
        original_name = volume['id']
        try:
            data = {'name': original_name}
            self.client.req('volumes', 'PUT', data, name=current_name)
        except exception.VolumeBackendAPIException:
            LOG.error('Unable to rename the logical volume '
                      'for volume: %s', original_name)
            # If the rename fails, _name_id should be set to the new
            # volume id and provider_location should be set to the
            # one from the new volume as well.
            name_id = new_volume['_name_id'] or new_volume['id']
            provider_location = new_volume['provider_location']

        return {'_name_id': name_id, 'provider_location': provider_location}

    def _update_volume_stats(self):
        sys = self.client.get_cluster()
        physical_space = int(sys["ud-ssd-space"]) / units.Mi
        used_physical_space = int(sys["ud-ssd-space-in-use"]) / units.Mi
        free_physical = physical_space - used_physical_space
        actual_prov = int(sys["vol-size"]) / units.Mi
        self._stats = {'volume_backend_name': self.backend_name,
                       'vendor_name': 'Dell EMC',
                       'driver_version': self.VERSION,
                       'storage_protocol': self.protocol,
                       'total_capacity_gb': physical_space,
                       'free_capacity_gb': free_physical,
                       'provisioned_capacity_gb': actual_prov,
                       'max_over_subscription_ratio': self.provisioning_factor,
                       'thin_provisioning_support': True,
                       'thick_provisioning_support': False,
                       'reserved_percentage':
                       self.configuration.reserved_percentage,
                       'QoS_support': False,
                       'multiattach': True,
                       }
        self._stats.update(self.client.get_extra_capabilities())

    def manage_existing(self, volume, existing_ref, is_snapshot=False):
        """Manages an existing LV."""
        lv_name = existing_ref['source-name']
        # Attempt to locate the volume.
        try:
            vol_obj = self.client.req('volumes', name=lv_name)['content']
            if (
                is_snapshot and
                (not vol_obj['ancestor-vol-id'] or
                 vol_obj['ancestor-vol-id'][XTREMIO_OID_NAME] !=
                 volume.volume_id)):
                kwargs = {'existing_ref': lv_name,
                          'reason': 'Not a snapshot of vol %s' %
                          volume.volume_id}
                raise exception.ManageExistingInvalidReference(**kwargs)
        except exception.NotFound:
            kwargs = {'existing_ref': lv_name,
                      'reason': 'Specified logical %s does not exist.' %
                      'snapshot' if is_snapshot else 'volume'}
            raise exception.ManageExistingInvalidReference(**kwargs)

        # Attempt to rename the LV to match the OpenStack internal name.
        self.client.req('volumes', 'PUT', data={'vol-name': volume['id']},
                        idx=vol_obj['index'])

    def manage_existing_get_size(self, volume, existing_ref,
                                 is_snapshot=False):
        """Return size of an existing LV for manage_existing."""
        # Check that the reference is valid
        if 'source-name' not in existing_ref:
            reason = _('Reference must contain source-name element.')
            raise exception.ManageExistingInvalidReference(
                existing_ref=existing_ref, reason=reason)
        lv_name = existing_ref['source-name']
        # Attempt to locate the volume.
        try:
            vol_obj = self.client.req('volumes', name=lv_name)['content']
        except exception.NotFound:
            kwargs = {'existing_ref': lv_name,
                      'reason': 'Specified logical %s does not exist.' %
                      'snapshot' if is_snapshot else 'volume'}
            raise exception.ManageExistingInvalidReference(**kwargs)
        # LV size is returned in gigabytes.  Attempt to parse size as a float
        # and round up to the next integer.
        lv_size = int(math.ceil(float(vol_obj['vol-size']) / units.Mi))

        return lv_size

    def unmanage(self, volume, is_snapshot=False):
        """Removes the specified volume from Cinder management."""
        # trying to rename the volume to [cinder name]-unmanged
        try:
            self.client.req('volumes', 'PUT', name=volume['id'],
                            data={'vol-name': volume['name'] + '-unmanged'})
        except exception.NotFound:
            LOG.info("%(typ)s with the name %(name)s wasn't found, "
                     "can't unmanage",
                     {'typ': 'Snapshot' if is_snapshot else 'Volume',
                      'name': volume['id']})
            raise exception.VolumeNotFound(volume_id=volume['id'])

    def manage_existing_snapshot(self, snapshot, existing_ref):
        self.manage_existing(snapshot, existing_ref, True)

    def manage_existing_snapshot_get_size(self, snapshot, existing_ref):
        return self.manage_existing_get_size(snapshot, existing_ref, True)

    def unmanage_snapshot(self, snapshot):
        self.unmanage(snapshot, True)

    def extend_volume(self, volume, new_size):
        """Extend an existing volume's size."""
        data = {'vol-size': str(new_size) + 'g'}
        try:
            self.client.req('volumes', 'PUT', data, name=volume['id'])
        except exception.NotFound:
            msg = _("can't find the volume to extend")
            raise exception.VolumeDriverException(message=msg)

    def check_for_export(self, context, volume_id):
        """Make sure volume is exported."""
        pass

    def terminate_connection(self, volume, connector, **kwargs):
        """Disallow connection from connector"""
        tg_index = '1'

        if not connector:
            vol = self.client.req('volumes', name=volume.id)['content']
            # foce detach, unmap all IGs from volume
            IG_OID = 0
            ig_indexes = [lun_map[IG_OID][XTREMIO_OID_INDEX] for
                          lun_map in vol['lun-mapping-list']]
            LOG.info('Force detach volume %(vol)s from luns %(luns)s.',
                     {'vol': vol['name'], 'luns': ig_indexes})
        else:
            host = connector['host']
            attachment_list = volume.volume_attachment
            LOG.debug("Volume attachment list: %(atl)s. "
                      "Attachment type: %(at)s",
                      {'atl': attachment_list, 'at': type(attachment_list)})
            try:
                att_list = attachment_list.objects
            except AttributeError:
                att_list = attachment_list
            if att_list is not None:
                host_list = [att.connector['host'] for att in att_list if
                             att is not None and att.connector is not None]
                current_host_occurances = host_list.count(host)
                if current_host_occurances > 1:
                    LOG.info("Volume is attached to multiple instances on "
                             "this host. Not removing the lun map.")
                    return
            vol = self.client.req('volumes', name=volume.id,
                                  data={'prop': 'index'})['content']
            ig_indexes = self._get_ig_indexes_from_initiators(connector)

        for ig_idx in ig_indexes:
            lm_name = '%s_%s_%s' % (str(vol['index']),
                                    str(ig_idx),
                                    tg_index)
            LOG.debug('Removing lun map %s.', lm_name)
            try:
                self.client.req('lun-maps', 'DELETE', name=lm_name)
            except exception.NotFound:
                LOG.warning("terminate_connection: lun map not found")

        if self.clean_ig:
            for idx in ig_indexes:
                try:
                    ig = self.client.req('initiator-groups', 'GET',
                                         {'prop': 'num-of-vols'},
                                         idx=idx)['content']
                    if ig['num-of-vols'] == 0:
                        self.client.req('initiator-groups', 'DELETE', idx=idx)
                except (exception.NotFound,
                        exception.VolumeBackendAPIException):
                    LOG.warning('Failed to clean IG %d without mappings', idx)

    def _get_password(self):
        return volume_utils.generate_password(
            length=12,
            symbolgroups=(string.ascii_uppercase + string.digits))

    def create_lun_map(self, volume, ig, lun_num=None):
        try:
            data = {'ig-id': ig, 'vol-id': volume['id']}
            if lun_num:
                data['lun'] = lun_num
            res = self.client.req('lun-maps', 'POST', data)

            lunmap = self._obj_from_result(res)
            LOG.info('Created lun-map:\n%s', lunmap)
        except XtremIOAlreadyMappedError:
            LOG.info('Volume already mapped, retrieving %(ig)s, %(vol)s',
                     {'ig': ig, 'vol': volume['id']})
            lunmap = self.client.find_lunmap(ig, volume['id'])
        return lunmap

    def _get_ig_name(self, connector):
        raise NotImplementedError()

    def _get_ig_indexes_from_initiators(self, connector):
        initiator_names = self._get_initiator_names(connector)
        return self.client.get_initiators_igs(initiator_names)

    def _get_initiator_names(self, connector):
        raise NotImplementedError()

    def create_consistencygroup(self, context, group):
        """Creates a consistency group.

        :param context: the context
        :param group: the group object to be created
        :returns: dict -- modelUpdate = {'status': 'available'}
        :raises: VolumeBackendAPIException
        """
        create_data = {'consistency-group-name': group['id']}
        self.client.req('consistency-groups', 'POST', data=create_data,
                        ver='v2')
        return {'status': fields.ConsistencyGroupStatus.AVAILABLE}

    def delete_consistencygroup(self, context, group, volumes):
        """Deletes a consistency group."""
        self.client.req('consistency-groups', 'DELETE', name=group['id'],
                        ver='v2')

        volumes_model_update = []

        for volume in volumes:
            self.delete_volume(volume)

            update_item = {'id': volume['id'],
                           'status': 'deleted'}

            volumes_model_update.append(update_item)

        model_update = {'status': group['status']}

        return model_update, volumes_model_update

    def _get_snapset_ancestors(self, snapset_name):
        snapset = self.client.req('snapshot-sets',
                                  name=snapset_name)['content']
        volume_ids = [s[XTREMIO_OID_INDEX] for s in snapset['vol-list']]
        return {v['ancestor-vol-id'][XTREMIO_OID_NAME]: v['name'] for v
                in self.client.req('volumes',
                                   data={'full': 1,
                                         'props':
                                         'ancestor-vol-id'})['volumes']
                if v['index'] in volume_ids}

    def create_consistencygroup_from_src(self, context, group, volumes,
                                         cgsnapshot=None, snapshots=None,
                                         source_cg=None, source_vols=None):
        """Creates a consistencygroup from source.

        :param context: the context of the caller.
        :param group: the dictionary of the consistency group to be created.
        :param volumes: a list of volume dictionaries in the group.
        :param cgsnapshot: the dictionary of the cgsnapshot as source.
        :param snapshots: a list of snapshot dictionaries in the cgsnapshot.
        :param source_cg: the dictionary of a consistency group as source.
        :param source_vols: a list of volume dictionaries in the source_cg.
        :returns: model_update, volumes_model_update
        """
        if not (cgsnapshot and snapshots and not source_cg or
                source_cg and source_vols and not cgsnapshot):
            msg = _("create_consistencygroup_from_src only supports a "
                    "cgsnapshot source or a consistency group source. "
                    "Multiple sources cannot be used.")
            raise exception.InvalidInput(msg)

        if cgsnapshot:
            snap_name = self._get_cgsnap_name(cgsnapshot)
            snap_by_anc = self._get_snapset_ancestors(snap_name)
            for volume, snapshot in zip(volumes, snapshots):
                real_snap = snap_by_anc[snapshot['volume_id']]
                self.create_volume_from_snapshot(
                    volume,
                    {'id': real_snap,
                     'volume_size': snapshot['volume_size']})

        elif source_cg:
            data = {'consistency-group-id': source_cg['id'],
                    'snapshot-set-name': group['id']}
            self.client.req('snapshots', 'POST', data, ver='v2')
            snap_by_anc = self._get_snapset_ancestors(group['id'])
            for volume, src_vol in zip(volumes, source_vols):
                snap_vol_name = snap_by_anc[src_vol['id']]
                self.client.req('volumes', 'PUT', {'name': volume['id']},
                                name=snap_vol_name)

        create_data = {'consistency-group-name': group['id'],
                       'vol-list': [v['id'] for v in volumes]}
        self.client.req('consistency-groups', 'POST', data=create_data,
                        ver='v2')

        return None, None

    def update_consistencygroup(self, context, group,
                                add_volumes=None, remove_volumes=None):
        """Updates a consistency group.

        :param context: the context of the caller.
        :param group: the dictionary of the consistency group to be updated.
        :param add_volumes: a list of volume dictionaries to be added.
        :param remove_volumes: a list of volume dictionaries to be removed.
        :returns: model_update, add_volumes_update, remove_volumes_update
        """
        add_volumes = add_volumes if add_volumes else []
        remove_volumes = remove_volumes if remove_volumes else []
        for vol in add_volumes:
            add_data = {'vol-id': vol['id'], 'cg-id': group['id']}
            self.client.req('consistency-group-volumes', 'POST', add_data,
                            ver='v2')
        for vol in remove_volumes:
            remove_data = {'vol-id': vol['id'], 'cg-id': group['id']}
            self.client.req('consistency-group-volumes', 'DELETE', remove_data,
                            name=group['id'], ver='v2')
        return None, None, None

    def _get_cgsnap_name(self, cgsnapshot):

        group_id = cgsnapshot.get('group_id')
        if group_id is None:
            group_id = cgsnapshot.get('consistencygroup_id')

        return '%(cg)s%(snap)s' % {'cg': group_id
                                   .replace('-', ''),
                                   'snap': cgsnapshot['id'].replace('-', '')}

    def create_cgsnapshot(self, context, cgsnapshot, snapshots):
        """Creates a cgsnapshot."""

        group_id = cgsnapshot.get('group_id')
        if group_id is None:
            group_id = cgsnapshot.get('consistencygroup_id')

        data = {'consistency-group-id': group_id,
                'snapshot-set-name': self._get_cgsnap_name(cgsnapshot)}
        self.client.req('snapshots', 'POST', data, ver='v2')

        return None, None

    def delete_cgsnapshot(self, context, cgsnapshot, snapshots):
        """Deletes a cgsnapshot."""
        self.client.req('snapshot-sets', 'DELETE',
                        name=self._get_cgsnap_name(cgsnapshot), ver='v2')
        return None, None

    def create_group(self, context, group):
        """Creates a group.

        :param context: the context of the caller.
        :param group: the group object.
        :returns: model_update
        """

        # the driver treats a group as a CG internally.
        # We proxy the calls to the CG api.
        return self.create_consistencygroup(context, group)

    def delete_group(self, context, group, volumes):
        """Deletes a group.

        :param context: the context of the caller.
        :param group: the group object.
        :param volumes: a list of volume objects in the group.
        :returns: model_update, volumes_model_update
        """

        # the driver treats a group as a CG internally.
        # We proxy the calls to the CG api.
        return self.delete_consistencygroup(context, group, volumes)

    def update_group(self, context, group,
                     add_volumes=None, remove_volumes=None):
        """Updates a group.

        :param context: the context of the caller.
        :param group: the group object.
        :param add_volumes: a list of volume objects to be added.
        :param remove_volumes: a list of volume objects to be removed.
        :returns: model_update, add_volumes_update, remove_volumes_update
        """

        # the driver treats a group as a CG internally.
        # We proxy the calls to the CG api.
        return self.update_consistencygroup(context, group, add_volumes,
                                            remove_volumes)

    def create_group_from_src(self, context, group, volumes,
                              group_snapshot=None, snapshots=None,
                              source_group=None, source_vols=None):
        """Creates a group from source.

        :param context: the context of the caller.
        :param group: the Group object to be created.
        :param volumes: a list of Volume objects in the group.
        :param group_snapshot: the GroupSnapshot object as source.
        :param snapshots: a list of snapshot objects in group_snapshot.
        :param source_group: the Group object as source.
        :param source_vols: a list of volume objects in the source_group.
        :returns: model_update, volumes_model_update
        """

        # the driver treats a group as a CG internally.
        # We proxy the calls to the CG api.
        return self.create_consistencygroup_from_src(context, group, volumes,
                                                     group_snapshot, snapshots,
                                                     source_group, source_vols)

    def create_group_snapshot(self, context, group_snapshot, snapshots):
        """Creates a group_snapshot.

        :param context: the context of the caller.
        :param group_snapshot: the GroupSnapshot object to be created.
        :param snapshots: a list of Snapshot objects in the group_snapshot.
        :returns: model_update, snapshots_model_update
        """

        # the driver treats a group as a CG internally.
        # We proxy the calls to the CG api.
        return self.create_cgsnapshot(context, group_snapshot, snapshots)

    def delete_group_snapshot(self, context, group_snapshot, snapshots):
        """Deletes a group_snapshot.

        :param context: the context of the caller.
        :param group_snapshot: the GroupSnapshot object to be deleted.
        :param snapshots: a list of snapshot objects in the group_snapshot.
        :returns: model_update, snapshots_model_update
        """

        # the driver treats a group as a CG internally.
        # We proxy the calls to the CG api.
        return self.delete_cgsnapshot(context, group_snapshot, snapshots)

    def _get_ig(self, name):
        try:
            return self.client.req('initiator-groups', 'GET',
                                   name=name)['content']
        except exception.NotFound:
            pass

    def _create_ig(self, name):
        # create an initiator group to hold the initiator
        data = {'ig-name': name}
        self.client.req('initiator-groups', 'POST', data)
        try:
            return self.client.req('initiator-groups', name=name)['content']
        except exception.NotFound:
            raise (exception.VolumeBackendAPIException
                   (data=_("Failed to create IG, %s") % name))

    def _port_is_allowed(self, port):
        """Check if port is in allowed ports list.

        If allowed ports are empty then all ports are allowed.

        :param port: iSCSI IP/FC WWN to check
        :return: is port allowed
        """

        if not self.allowed_ports:
            return True
        return port.lower() in self.allowed_ports


@interface.volumedriver
class XtremIOISCSIDriver(XtremIOVolumeDriver, driver.ISCSIDriver):
    """Executes commands relating to ISCSI volumes.

    We make use of model provider properties as follows:

    ``provider_location``
      if present, contains the iSCSI target information in the same
      format as an ietadm discovery
      i.e. '<ip>:<port>,<portal> <target IQN>'

    ``provider_auth``
      if present, contains a space-separated triple:
      '<auth method> <auth username> <auth password>'.
      `CHAP` is the only auth_method in use at the moment.
    """
    driver_name = 'XtremIO_ISCSI'

    def __init__(self, *args, **kwargs):
        super(XtremIOISCSIDriver, self).__init__(*args, **kwargs)
        self.protocol = constants.ISCSI

    def _add_auth(self, data, login_chap, discovery_chap):
        login_passwd, discovery_passwd = None, None
        if login_chap:
            data['initiator-authentication-user-name'] = 'chap_user'
            login_passwd = self._get_password()
            data['initiator-authentication-password'] = login_passwd
        if discovery_chap:
            data['initiator-discovery-user-name'] = 'chap_user'
            discovery_passwd = self._get_password()
            data['initiator-discovery-password'] = discovery_passwd
        return login_passwd, discovery_passwd

    def _create_initiator(self, connector, login_chap, discovery_chap):
        initiator = self._get_initiator_names(connector)[0]
        # create an initiator
        data = {'initiator-name': initiator,
                'ig-id': initiator,
                'port-address': initiator}
        l, d = self._add_auth(data, login_chap, discovery_chap)
        self.client.req('initiators', 'POST', data)
        return l, d

    def initialize_connection(self, volume, connector):
        try:
            sys = self.client.get_cluster()
        except exception.NotFound:
            msg = _("XtremIO not initialized correctly, no clusters found")
            raise exception.VolumeBackendAPIException(data=msg)
        login_chap = (sys.get('chap-authentication-mode', 'disabled') !=
                      'disabled')
        discovery_chap = (sys.get('chap-discovery-mode', 'disabled') !=
                          'disabled')
        initiator_name = self._get_initiator_names(connector)[0]
        initiator = self.client.get_initiator(initiator_name)
        if initiator:
            login_passwd = initiator['chap-authentication-initiator-password']
            discovery_passwd = initiator['chap-discovery-initiator-password']
            ig = self._get_ig(initiator['ig-id'][XTREMIO_OID_NAME])
        else:
            ig = self._get_ig(self._get_ig_name(connector))
            if not ig:
                ig = self._create_ig(self._get_ig_name(connector))
            (login_passwd,
             discovery_passwd) = self._create_initiator(connector,
                                                        login_chap,
                                                        discovery_chap)
        # if CHAP was enabled after the initiator was created
        if login_chap and not login_passwd:
            LOG.info('Initiator has no password while using chap, adding it.')
            data = {}
            (login_passwd,
             d_passwd) = self._add_auth(data, login_chap, discovery_chap and
                                        not discovery_passwd)
            discovery_passwd = (discovery_passwd if discovery_passwd
                                else d_passwd)
            self.client.req('initiators', 'PUT', data, idx=initiator['index'])

        # lun mappping
        lunmap = self.create_lun_map(volume, ig['ig-id'][XTREMIO_OID_NAME])

        properties = self._get_iscsi_properties(lunmap)

        if login_chap:
            properties['auth_method'] = 'CHAP'
            properties['auth_username'] = 'chap_user'
            properties['auth_password'] = login_passwd
        if discovery_chap:
            properties['discovery_auth_method'] = 'CHAP'
            properties['discovery_auth_username'] = 'chap_user'
            properties['discovery_auth_password'] = discovery_passwd
        LOG.debug('init conn params:\n%s',
                  strutils.mask_dict_password(properties))
        return {
            'driver_volume_type': 'iscsi',
            'data': properties
        }

    def _get_iscsi_properties(self, lunmap):
        """Gets iscsi configuration.

        :target_discovered:    boolean indicating whether discovery was used
        :target_iqn:    the IQN of the iSCSI target
        :target_portal:    the portal of the iSCSI target
        :target_lun:    the lun of the iSCSI target
        :volume_id:    the id of the volume (currently used by xen)
        :auth_method:, :auth_username:, :auth_password:
            the authentication details. Right now, either auth_method is not
            present meaning no authentication, or auth_method == `CHAP`
            meaning use CHAP with the specified credentials.
        multiple connection return
        :target_iqns, :target_portals, :target_luns, which contain lists of
        multiple values. The main portal information is also returned in
        :target_iqn, :target_portal, :target_lun for backward compatibility.
        """
        iscsi_portals = self.client.get_iscsi_portals()
        allowed_portals = []
        for iscsi_portal in iscsi_portals:
            iscsi_portal['ip-addr'] = iscsi_portal['ip-addr'].split('/')[0]
            if self._port_is_allowed(iscsi_portal['ip-addr']):
                allowed_portals.append(iscsi_portal)
        if not allowed_portals:
            msg = _("There are no accessible iSCSI targets on the "
                    "system.")
            raise exception.VolumeBackendAPIException(data=msg)
        portal = RANDOM.choice(allowed_portals)
        portal_addr = ('%(ip)s:%(port)d' %
                       {'ip': netutils.escape_ipv6(portal['ip-addr']),
                        'port': portal['ip-port']})

        tg_portals = ['%(ip)s:%(port)d' %
                      {'ip': netutils.escape_ipv6(p['ip-addr']),
                       'port': p['ip-port']}
                      for p in allowed_portals]
        properties = {'target_discovered': False,
                      'target_iqn': portal['port-address'],
                      'target_lun': lunmap['lun'],
                      'target_portal': portal_addr,
                      'target_iqns': [
                          p['port-address'] for p in allowed_portals
                      ],
                      'target_portals': tg_portals,
                      'target_luns': [lunmap['lun']] * len(allowed_portals)}
        return properties

    def _get_initiator_names(self, connector):
        return [connector['initiator']]

    def _get_ig_name(self, connector):
        return connector['initiator']


@interface.volumedriver
class XtremIOFCDriver(XtremIOVolumeDriver,
                      driver.FibreChannelDriver):

    def __init__(self, *args, **kwargs):
        super(XtremIOFCDriver, self).__init__(*args, **kwargs)
        self.protocol = constants.FC
        self._targets = None

    def get_targets(self):
        if not self._targets:
            try:
                targets = self.client.get_fc_up_ports()
                allowed_targets = []
                for target in targets:
                    if self._port_is_allowed(target['port-address']):
                        allowed_targets.append(
                            target['port-address'].replace(':', '')
                        )
                if not allowed_targets:
                    msg = _("There are no accessible Fibre Channel targets "
                            "on the system.")
                    raise exception.VolumeBackendAPIException(data=msg)
                self._targets = allowed_targets
            except exception.NotFound:
                raise (exception.VolumeBackendAPIException
                       (data=_("Failed to get targets")))
        return self._targets

    def _get_free_lun(self, igs):
        luns = []
        for ig in igs:
            luns.extend(lm['lun'] for lm in
                        self.client.req('lun-maps',
                                        data={'full': 1, 'prop': 'lun',
                                              'filter': 'ig-name:eq:%s' % ig})
                        ['lun-maps'])
        uniq_luns = set(luns + [0])
        seq = range(len(uniq_luns) + 1)
        return min(set(seq) - uniq_luns)

    def initialize_connection(self, volume, connector):
        wwpns = self._get_initiator_names(connector)
        ig_name = self._get_ig_name(connector)
        i_t_map = {}
        found = []
        new = []
        for wwpn in wwpns:
            init = self.client.get_initiator(wwpn)
            if init:
                found.append(init)
            else:
                new.append(wwpn)
            i_t_map[wwpn.replace(':', '')] = self.get_targets()
        # get or create initiator group
        if new:
            ig = self._get_ig(ig_name)
            if not ig:
                ig = self._create_ig(ig_name)
            for wwpn in new:
                data = {'initiator-name': wwpn, 'ig-id': ig_name,
                        'port-address': wwpn}
                self.client.req('initiators', 'POST', data)
        igs = list(set([i['ig-id'][XTREMIO_OID_NAME] for i in found]))
        if new and ig['ig-id'][XTREMIO_OID_NAME] not in igs:
            igs.append(ig['ig-id'][XTREMIO_OID_NAME])

        if len(igs) > 1:
            lun_num = self._get_free_lun(igs)
        else:
            lun_num = None
        for ig in igs:
            lunmap = self.create_lun_map(volume, ig, lun_num)
            lun_num = lunmap['lun']
        conn_info = {'driver_volume_type': 'fibre_channel',
                     'data': {
                         'target_discovered': False,
                         'target_lun': lun_num,
                         'target_wwn': self.get_targets(),
                         'initiator_target_map': i_t_map}}
        fczm_utils.add_fc_zone(conn_info)
        return conn_info

    def terminate_connection(self, volume, connector, **kwargs):
        (super(XtremIOFCDriver, self)
         .terminate_connection(volume, connector, **kwargs))
        has_volumes = (not connector
                       or self.client.
                       num_of_mapped_volumes(self._get_ig_name(connector)) > 0)

        if has_volumes:
            data = {}
        else:
            i_t_map = {}
            for initiator in self._get_initiator_names(connector):
                i_t_map[initiator.replace(':', '')] = self.get_targets()
            data = {'target_wwn': self.get_targets(),
                    'initiator_target_map': i_t_map}

        conn_info = {'driver_volume_type': 'fibre_channel',
                     'data': data}
        fczm_utils.remove_fc_zone(conn_info)
        return conn_info

    def _get_initiator_names(self, connector):
        return [wwpn if ':' in wwpn else
                ':'.join(wwpn[i:i + 2] for i in range(0, len(wwpn), 2))
                for wwpn in connector['wwpns']]

    def _get_ig_name(self, connector):
        return connector['host']
