# Copyright (c) 2012 - 2014 EMC Corporation.
# 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.
supported 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
"""

import json
import math
import random
import requests
import string

from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import strutils
from oslo_utils import units
import six
from six.moves import http_client

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 utils as vutils
from cinder.zonemanager import utils as fczm_utils


LOG = logging.getLogger(__name__)

CONF = cfg.CONF
DEFAULT_PROVISIONING_FACTOR = 20.0
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.')]

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 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(exception.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') % six.text_type(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 exception.XtremIOAlreadyMappedError()
            elif err_msg == SYSTEM_BUSY:
                raise exception.XtremIOArrayBusy()
            elif err_msg in (TOO_MANY_OBJECTS, TOO_MANY_SNAPSHOTS_PER_VOL):
                raise exception.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.10'

    # ThirdPartySystems wiki
    CI_WIKI_NAME = "EMC_XIO_CI"

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

    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 = vutils.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.client = XtremIOClient3(self.configuration, self.cluster_id)

    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 exception.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.CinderException('Exceeded the configured limit of '
                                            '%d snapshots per volume' % limit)
        try:
            self.client.create_snapshot(src_vref['id'], volume['id'])
        except exception.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 *
                                            self.provisioning_factor),
                       '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': False,
                       }
        self._stats.update(self.client.get_extra_capabilities())

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

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

    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': six.text_type(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:
            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' % (six.text_type(vol['index']),
                                    six.text_type(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 ''.join(RANDOM.choice
                       (string.ascii_uppercase + string.digits)
                       for _ in range(12))

    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 exception.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))


@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 = '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.
        """
        portals = self.client.get_iscsi_portals()
        if not portals:
            msg = _("XtremIO not configured correctly, no iscsi portals found")
            LOG.error(msg)
            raise exception.VolumeDriverException(message=msg)
        portal = RANDOM.choice(portals)
        portal_addr = ('%(ip)s:%(port)d' %
                       {'ip': portal['ip-addr'].split('/')[0],
                        'port': portal['ip-port']})

        tg_portals = ['%(ip)s:%(port)d' % {'ip': p['ip-addr'].split('/')[0],
                                           'port': p['ip-port']}
                      for p in 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 portals],
                      'target_portals': tg_portals,
                      'target_luns': [lunmap['lun']] * len(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 = 'FC'
        self._targets = None

    def get_targets(self):
        if not self._targets:
            try:
                targets = self.client.get_fc_up_ports()
                self._targets = [target['port-address'].replace(':', '')
                                 for target in 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)

    @fczm_utils.add_fc_zone
    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']
        return {'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.remove_fc_zone
    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}

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

    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']
