1# Copyright (c) 2012 - 2014 EMC Corporation.
2# All Rights Reserved.
3#
4#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5#    not use this file except in compliance with the License. You may obtain
6#    a copy of the License at
7#
8#         http://www.apache.org/licenses/LICENSE-2.0
9#
10#    Unless required by applicable law or agreed to in writing, software
11#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13#    License for the specific language governing permissions and limitations
14#    under the License.
15"""
16Driver for Dell EMC XtremIO Storage.
17supported XtremIO version 2.4 and up
18
19.. code-block:: none
20
21  1.0.0 - initial release
22  1.0.1 - enable volume extend
23  1.0.2 - added FC support, improved error handling
24  1.0.3 - update logging level, add translation
25  1.0.4 - support for FC zones
26  1.0.5 - add support for XtremIO 4.0
27  1.0.6 - add support for iSCSI multipath, CA validation, consistency groups,
28          R/O snapshots, CHAP discovery authentication
29  1.0.7 - cache glance images on the array
30  1.0.8 - support for volume retype, CG fixes
31  1.0.9 - performance improvements, support force detach, support for X2
32  1.0.10 - option to clean unused IGs
33"""
34
35import json
36import math
37import random
38import requests
39import string
40
41from oslo_config import cfg
42from oslo_log import log as logging
43from oslo_utils import strutils
44from oslo_utils import units
45import six
46from six.moves import http_client
47
48from cinder import context
49from cinder import exception
50from cinder.i18n import _
51from cinder import interface
52from cinder.objects import fields
53from cinder import utils
54from cinder.volume import configuration
55from cinder.volume import driver
56from cinder.volume.drivers.san import san
57from cinder.volume import utils as vutils
58from cinder.zonemanager import utils as fczm_utils
59
60
61LOG = logging.getLogger(__name__)
62
63CONF = cfg.CONF
64DEFAULT_PROVISIONING_FACTOR = 20.0
65XTREMIO_OPTS = [
66    cfg.StrOpt('xtremio_cluster_name',
67               default='',
68               help='XMS cluster id in multi-cluster environment'),
69    cfg.IntOpt('xtremio_array_busy_retry_count',
70               default=5,
71               help='Number of retries in case array is busy'),
72    cfg.IntOpt('xtremio_array_busy_retry_interval',
73               default=5,
74               help='Interval between retries in case array is busy'),
75    cfg.IntOpt('xtremio_volumes_per_glance_cache',
76               default=100,
77               help='Number of volumes created from each cached glance image'),
78    cfg.BoolOpt('xtremio_clean_unused_ig',
79                default=False,
80                help='Should the driver remove initiator groups with no '
81                     'volumes after the last connection was terminated. '
82                     'Since the behavior till now was to leave '
83                     'the IG be, we default to False (not deleting IGs '
84                     'without connected volumes); setting this parameter '
85                     'to True will remove any IG after terminating its '
86                     'connection to the last volume.')]
87
88CONF.register_opts(XTREMIO_OPTS, group=configuration.SHARED_CONF_GROUP)
89
90RANDOM = random.Random()
91OBJ_NOT_FOUND_ERR = 'obj_not_found'
92VOL_NOT_UNIQUE_ERR = 'vol_obj_name_not_unique'
93VOL_OBJ_NOT_FOUND_ERR = 'vol_obj_not_found'
94ALREADY_MAPPED_ERR = 'already_mapped'
95SYSTEM_BUSY = 'system_is_busy'
96TOO_MANY_OBJECTS = 'too_many_objs'
97TOO_MANY_SNAPSHOTS_PER_VOL = 'too_many_snapshots_per_vol'
98
99
100XTREMIO_OID_NAME = 1
101XTREMIO_OID_INDEX = 2
102
103
104class XtremIOClient(object):
105    def __init__(self, configuration, cluster_id):
106        self.configuration = configuration
107        self.cluster_id = cluster_id
108        self.verify = (self.configuration.
109                       safe_get('driver_ssl_cert_verify') or False)
110        if self.verify:
111            verify_path = (self.configuration.
112                           safe_get('driver_ssl_cert_path') or None)
113            if verify_path:
114                self.verify = verify_path
115
116    def get_base_url(self, ver):
117        if ver == 'v1':
118            return 'https://%s/api/json/types' % self.configuration.san_ip
119        elif ver == 'v2':
120            return 'https://%s/api/json/v2/types' % self.configuration.san_ip
121
122    def req(self, object_type='volumes', method='GET', data=None,
123            name=None, idx=None, ver='v1'):
124        @utils.retry(exception.XtremIOArrayBusy,
125                     self.configuration.xtremio_array_busy_retry_count,
126                     self.configuration.xtremio_array_busy_retry_interval, 1)
127        def _do_req(object_type, method, data, name, idx, ver):
128            if not data:
129                data = {}
130            if name and idx:
131                msg = _("can't handle both name and index in req")
132                LOG.error(msg)
133                raise exception.VolumeDriverException(message=msg)
134
135            url = '%s/%s' % (self.get_base_url(ver), object_type)
136            params = {}
137            key = None
138            if name:
139                params['name'] = name
140                key = name
141            elif idx:
142                url = '%s/%d' % (url, idx)
143                key = str(idx)
144            if method in ('GET', 'DELETE'):
145                params.update(data)
146                self.update_url(params, self.cluster_id)
147            if method != 'GET':
148                self.update_data(data, self.cluster_id)
149                # data may include chap password
150                LOG.debug('data: %s', strutils.mask_password(data))
151            LOG.debug('%(type)s %(url)s', {'type': method, 'url': url})
152            try:
153                response = requests.request(
154                    method, url, params=params, data=json.dumps(data),
155                    verify=self.verify, auth=(self.configuration.san_login,
156                                              self.configuration.san_password))
157            except requests.exceptions.RequestException as exc:
158                msg = (_('Exception: %s') % six.text_type(exc))
159                raise exception.VolumeDriverException(message=msg)
160
161            if (http_client.OK <= response.status_code <
162                    http_client.MULTIPLE_CHOICES):
163                if method in ('GET', 'POST'):
164                    return response.json()
165                else:
166                    return ''
167
168            self.handle_errors(response, key, object_type)
169        return _do_req(object_type, method, data, name, idx, ver)
170
171    def handle_errors(self, response, key, object_type):
172        if response.status_code == http_client.BAD_REQUEST:
173            error = response.json()
174            err_msg = error.get('message')
175            if err_msg.endswith(OBJ_NOT_FOUND_ERR):
176                LOG.warning("object %(key)s of "
177                            "type %(typ)s not found, %(err_msg)s",
178                            {'key': key, 'typ': object_type,
179                             'err_msg': err_msg, })
180                raise exception.NotFound()
181            elif err_msg == VOL_NOT_UNIQUE_ERR:
182                LOG.error("can't create 2 volumes with the same name, %s",
183                          err_msg)
184                msg = _('Volume by this name already exists')
185                raise exception.VolumeBackendAPIException(data=msg)
186            elif err_msg == VOL_OBJ_NOT_FOUND_ERR:
187                LOG.error("Can't find volume to map %(key)s, %(msg)s",
188                          {'key': key, 'msg': err_msg, })
189                raise exception.VolumeNotFound(volume_id=key)
190            elif ALREADY_MAPPED_ERR in err_msg:
191                raise exception.XtremIOAlreadyMappedError()
192            elif err_msg == SYSTEM_BUSY:
193                raise exception.XtremIOArrayBusy()
194            elif err_msg in (TOO_MANY_OBJECTS, TOO_MANY_SNAPSHOTS_PER_VOL):
195                raise exception.XtremIOSnapshotsLimitExceeded()
196        msg = _('Bad response from XMS, %s') % response.text
197        LOG.error(msg)
198        raise exception.VolumeBackendAPIException(message=msg)
199
200    def update_url(self, data, cluster_id):
201        return
202
203    def update_data(self, data, cluster_id):
204        return
205
206    def get_cluster(self):
207        return self.req('clusters', idx=1)['content']
208
209    def create_snapshot(self, src, dest, ro=False):
210        """Create a snapshot of a volume on the array.
211
212        XtreamIO array snapshots are also volumes.
213
214        :src: name of the source volume to be cloned
215        :dest: name for the new snapshot
216        :ro: new snapshot type ro/regular. only applicable to Client4
217        """
218        raise NotImplementedError()
219
220    def get_extra_capabilities(self):
221        return {}
222
223    def get_initiator(self, port_address):
224        raise NotImplementedError()
225
226    def add_vol_to_cg(self, vol_id, cg_id):
227        pass
228
229    def get_initiators_igs(self, port_addresses):
230        ig_indexes = set()
231        for port_address in port_addresses:
232            initiator = self.get_initiator(port_address)
233            ig_indexes.add(initiator['ig-id'][XTREMIO_OID_INDEX])
234
235        return list(ig_indexes)
236
237    def get_fc_up_ports(self):
238        targets = [self.req('targets', name=target['name'])['content']
239                   for target in self.req('targets')['targets']]
240        return [target for target in targets
241                if target['port-type'] == 'fc' and
242                target["port-state"] == 'up']
243
244
245class XtremIOClient3(XtremIOClient):
246    def __init__(self, configuration, cluster_id):
247        super(XtremIOClient3, self).__init__(configuration, cluster_id)
248        self._portals = []
249
250    def find_lunmap(self, ig_name, vol_name):
251        try:
252            lun_mappings = self.req('lun-maps')['lun-maps']
253        except exception.NotFound:
254            raise (exception.VolumeDriverException
255                   (_("can't find lun-map, ig:%(ig)s vol:%(vol)s") %
256                    {'ig': ig_name, 'vol': vol_name}))
257
258        for lm_link in lun_mappings:
259            idx = lm_link['href'].split('/')[-1]
260            # NOTE(geguileo): There can be races so mapped elements retrieved
261            # in the listing may no longer exist.
262            try:
263                lm = self.req('lun-maps', idx=int(idx))['content']
264            except exception.NotFound:
265                continue
266            if lm['ig-name'] == ig_name and lm['vol-name'] == vol_name:
267                return lm
268
269        return None
270
271    def num_of_mapped_volumes(self, initiator):
272        cnt = 0
273        for lm_link in self.req('lun-maps')['lun-maps']:
274            idx = lm_link['href'].split('/')[-1]
275            # NOTE(geguileo): There can be races so mapped elements retrieved
276            # in the listing may no longer exist.
277            try:
278                lm = self.req('lun-maps', idx=int(idx))['content']
279            except exception.NotFound:
280                continue
281            if lm['ig-name'] == initiator:
282                cnt += 1
283        return cnt
284
285    def get_iscsi_portals(self):
286        if self._portals:
287            return self._portals
288
289        iscsi_portals = [t['name'] for t in self.req('iscsi-portals')
290                         ['iscsi-portals']]
291        for portal_name in iscsi_portals:
292            try:
293                self._portals.append(self.req('iscsi-portals',
294                                              name=portal_name)['content'])
295            except exception.NotFound:
296                raise (exception.VolumeBackendAPIException
297                       (data=_("iscsi portal, %s, not found") % portal_name))
298
299        return self._portals
300
301    def create_snapshot(self, src, dest, ro=False):
302        data = {'snap-vol-name': dest, 'ancestor-vol-id': src}
303
304        self.req('snapshots', 'POST', data)
305
306    def get_initiator(self, port_address):
307        try:
308            return self.req('initiators', 'GET', name=port_address)['content']
309        except exception.NotFound:
310            pass
311
312
313class XtremIOClient4(XtremIOClient):
314    def __init__(self, configuration, cluster_id):
315        super(XtremIOClient4, self).__init__(configuration, cluster_id)
316        self._cluster_name = None
317
318    def req(self, object_type='volumes', method='GET', data=None,
319            name=None, idx=None, ver='v2'):
320        return super(XtremIOClient4, self).req(object_type, method, data,
321                                               name, idx, ver)
322
323    def get_extra_capabilities(self):
324        return {'consistencygroup_support': True}
325
326    def find_lunmap(self, ig_name, vol_name):
327        try:
328            return (self.req('lun-maps',
329                             data={'full': 1,
330                                   'filter': ['vol-name:eq:%s' % vol_name,
331                                              'ig-name:eq:%s' % ig_name]})
332                    ['lun-maps'][0])
333        except (KeyError, IndexError):
334            raise exception.VolumeNotFound(volume_id=vol_name)
335
336    def num_of_mapped_volumes(self, initiator):
337        return len(self.req('lun-maps',
338                            data={'filter': 'ig-name:eq:%s' % initiator})
339                   ['lun-maps'])
340
341    def update_url(self, data, cluster_id):
342        if cluster_id:
343            data['cluster-name'] = cluster_id
344
345    def update_data(self, data, cluster_id):
346        if cluster_id:
347            data['cluster-id'] = cluster_id
348
349    def get_iscsi_portals(self):
350        return self.req('iscsi-portals',
351                        data={'full': 1})['iscsi-portals']
352
353    def get_cluster(self):
354        if not self.cluster_id:
355            self.cluster_id = self.req('clusters')['clusters'][0]['name']
356
357        return self.req('clusters', name=self.cluster_id)['content']
358
359    def create_snapshot(self, src, dest, ro=False):
360        data = {'snapshot-set-name': dest, 'snap-suffix': dest,
361                'volume-list': [src],
362                'snapshot-type': 'readonly' if ro else 'regular'}
363
364        res = self.req('snapshots', 'POST', data, ver='v2')
365        typ, idx = res['links'][0]['href'].split('/')[-2:]
366
367        # rename the snapshot
368        data = {'name': dest}
369        try:
370            self.req(typ, 'PUT', data, idx=int(idx))
371        except exception.VolumeBackendAPIException:
372            # reverting
373            LOG.error('Failed to rename the created snapshot, reverting.')
374            self.req(typ, 'DELETE', idx=int(idx))
375            raise
376
377    def add_vol_to_cg(self, vol_id, cg_id):
378        add_data = {'vol-id': vol_id, 'cg-id': cg_id}
379        self.req('consistency-group-volumes', 'POST', add_data, ver='v2')
380
381    def get_initiator(self, port_address):
382        inits = self.req('initiators',
383                         data={'filter': 'port-address:eq:' + port_address,
384                               'full': 1})['initiators']
385        if len(inits) == 1:
386            return inits[0]
387        else:
388            pass
389
390    def get_fc_up_ports(self):
391        return self.req('targets',
392                        data={'full': 1,
393                              'filter': ['port-type:eq:fc',
394                                         'port-state:eq:up'],
395                              'prop': 'port-address'})["targets"]
396
397
398class XtremIOClient42(XtremIOClient4):
399    def get_initiators_igs(self, port_addresses):
400        init_filter = ','.join('port-address:eq:{}'.format(port_address) for
401                               port_address in port_addresses)
402        initiators = self.req('initiators',
403                              data={'filter': init_filter,
404                                    'full': 1, 'prop': 'ig-id'})['initiators']
405        return list(set(ig_id['ig-id'][XTREMIO_OID_INDEX]
406                        for ig_id in initiators))
407
408
409class XtremIOVolumeDriver(san.SanDriver):
410    """Executes commands relating to Volumes."""
411
412    VERSION = '1.0.10'
413
414    # ThirdPartySystems wiki
415    CI_WIKI_NAME = "EMC_XIO_CI"
416
417    driver_name = 'XtremIO'
418    MIN_XMS_VERSION = [3, 0, 0]
419
420    def __init__(self, *args, **kwargs):
421        super(XtremIOVolumeDriver, self).__init__(*args, **kwargs)
422        self.configuration.append_config_values(XTREMIO_OPTS)
423        self.protocol = None
424        self.backend_name = (self.configuration.safe_get('volume_backend_name')
425                             or self.driver_name)
426        self.cluster_id = (self.configuration.safe_get('xtremio_cluster_name')
427                           or '')
428        self.provisioning_factor = vutils.get_max_over_subscription_ratio(
429            self.configuration.max_over_subscription_ratio,
430            supports_auto=False)
431
432        self.clean_ig = (self.configuration.safe_get('xtremio_clean_unused_ig')
433                         or False)
434        self._stats = {}
435        self.client = XtremIOClient3(self.configuration, self.cluster_id)
436
437    def _obj_from_result(self, res):
438        typ, idx = res['links'][0]['href'].split('/')[-2:]
439        return self.client.req(typ, idx=int(idx))['content']
440
441    def check_for_setup_error(self):
442        try:
443            name = self.client.req('clusters')['clusters'][0]['name']
444            cluster = self.client.req('clusters', name=name)['content']
445            version_text = cluster['sys-sw-version']
446        except exception.NotFound:
447            msg = _("XtremIO not initialized correctly, no clusters found")
448            raise (exception.VolumeBackendAPIException
449                   (data=msg))
450        ver = [int(n) for n in version_text.split('-')[0].split('.')]
451        if ver < self.MIN_XMS_VERSION:
452            msg = (_('Invalid XtremIO version %(cur)s,'
453                     ' version %(min)s or up is required') %
454                   {'min': self.MIN_XMS_VERSION,
455                    'cur': ver})
456            LOG.error(msg)
457            raise exception.VolumeBackendAPIException(data=msg)
458        else:
459            LOG.info('XtremIO Cluster version %s', version_text)
460        client_ver = '3'
461        if ver[0] >= 4:
462            # get XMS version
463            xms = self.client.req('xms', idx=1)['content']
464            xms_version = tuple([int(i) for i in
465                                 xms['sw-version'].split('-')[0].split('.')])
466            LOG.info('XtremIO XMS version %s', version_text)
467            if xms_version >= (4, 2):
468                self.client = XtremIOClient42(self.configuration,
469                                              self.cluster_id)
470                client_ver = '4.2'
471            else:
472                self.client = XtremIOClient4(self.configuration,
473                                             self.cluster_id)
474                client_ver = '4'
475        LOG.info('Using XtremIO Client %s', client_ver)
476
477    def create_volume(self, volume):
478        """Creates a volume."""
479        data = {'vol-name': volume['id'],
480                'vol-size': str(volume['size']) + 'g'
481                }
482        self.client.req('volumes', 'POST', data)
483
484        # Add the volume to a cg in case volume requested a cgid or group_id.
485        # If both cg_id and group_id exists in a volume. group_id will take
486        # place.
487
488        consistency_group = volume.get('consistencygroup_id')
489
490        # if cg_id and group_id are both exists, we gives priority to group_id.
491        if volume.get('group_id'):
492            consistency_group = volume.get('group_id')
493
494        if consistency_group:
495            self.client.add_vol_to_cg(volume['id'],
496                                      consistency_group)
497
498    def create_volume_from_snapshot(self, volume, snapshot):
499        """Creates a volume from a snapshot."""
500        if snapshot.get('cgsnapshot_id'):
501            # get array snapshot id from CG snapshot
502            snap_by_anc = self._get_snapset_ancestors(snapshot.cgsnapshot)
503            snapshot_id = snap_by_anc[snapshot['volume_id']]
504        else:
505            snapshot_id = snapshot['id']
506
507        try:
508            self.client.create_snapshot(snapshot_id, volume['id'])
509        except exception.XtremIOSnapshotsLimitExceeded as e:
510            raise exception.CinderException(e.message)
511
512        # extend the snapped volume if requested size is larger then original
513        if volume['size'] > snapshot['volume_size']:
514            try:
515                self.extend_volume(volume, volume['size'])
516            except Exception:
517                LOG.error('failed to extend volume %s, '
518                          'reverting volume from snapshot operation',
519                          volume['id'])
520                # remove the volume in case resize failed
521                self.delete_volume(volume)
522                raise
523
524        # add new volume to consistency group
525        if (volume.get('consistencygroup_id') and
526                self.client is XtremIOClient4):
527            self.client.add_vol_to_cg(volume['id'],
528                                      snapshot['consistencygroup_id'])
529
530    def create_cloned_volume(self, volume, src_vref):
531        """Creates a clone of the specified volume."""
532        vol = self.client.req('volumes', name=src_vref['id'])['content']
533        ctxt = context.get_admin_context()
534        cache = self.db.image_volume_cache_get_by_volume_id(ctxt,
535                                                            src_vref['id'])
536        limit = self.configuration.safe_get('xtremio_volumes_per_glance_cache')
537        if cache and limit and limit > 0 and limit <= vol['num-of-dest-snaps']:
538            raise exception.CinderException('Exceeded the configured limit of '
539                                            '%d snapshots per volume' % limit)
540        try:
541            self.client.create_snapshot(src_vref['id'], volume['id'])
542        except exception.XtremIOSnapshotsLimitExceeded as e:
543            raise exception.CinderException(e.message)
544
545        # extend the snapped volume if requested size is larger then original
546        if volume['size'] > src_vref['size']:
547            try:
548                self.extend_volume(volume, volume['size'])
549            except Exception:
550                LOG.error('failed to extend volume %s, '
551                          'reverting clone operation', volume['id'])
552                # remove the volume in case resize failed
553                self.delete_volume(volume)
554                raise
555
556        if volume.get('consistencygroup_id') and self.client is XtremIOClient4:
557            self.client.add_vol_to_cg(volume['id'],
558                                      volume['consistencygroup_id'])
559
560    def delete_volume(self, volume):
561        """Deletes a volume."""
562        try:
563            self.client.req('volumes', 'DELETE', name=volume.name_id)
564        except exception.NotFound:
565            LOG.info("volume %s doesn't exist", volume.name_id)
566
567    def create_snapshot(self, snapshot):
568        """Creates a snapshot."""
569        self.client.create_snapshot(snapshot.volume_id, snapshot.id, True)
570
571    def delete_snapshot(self, snapshot):
572        """Deletes a snapshot."""
573        try:
574            self.client.req('volumes', 'DELETE', name=snapshot.id)
575        except exception.NotFound:
576            LOG.info("snapshot %s doesn't exist", snapshot.id)
577
578    def update_migrated_volume(self, ctxt, volume, new_volume,
579                               original_volume_status):
580        # as the volume name is used to id the volume we need to rename it
581        name_id = None
582        provider_location = None
583        current_name = new_volume['id']
584        original_name = volume['id']
585        try:
586            data = {'name': original_name}
587            self.client.req('volumes', 'PUT', data, name=current_name)
588        except exception.VolumeBackendAPIException:
589            LOG.error('Unable to rename the logical volume '
590                      'for volume: %s', original_name)
591            # If the rename fails, _name_id should be set to the new
592            # volume id and provider_location should be set to the
593            # one from the new volume as well.
594            name_id = new_volume['_name_id'] or new_volume['id']
595            provider_location = new_volume['provider_location']
596
597        return {'_name_id': name_id, 'provider_location': provider_location}
598
599    def _update_volume_stats(self):
600        sys = self.client.get_cluster()
601        physical_space = int(sys["ud-ssd-space"]) / units.Mi
602        used_physical_space = int(sys["ud-ssd-space-in-use"]) / units.Mi
603        free_physical = physical_space - used_physical_space
604        actual_prov = int(sys["vol-size"]) / units.Mi
605        self._stats = {'volume_backend_name': self.backend_name,
606                       'vendor_name': 'Dell EMC',
607                       'driver_version': self.VERSION,
608                       'storage_protocol': self.protocol,
609                       'total_capacity_gb': physical_space,
610                       'free_capacity_gb': (free_physical *
611                                            self.provisioning_factor),
612                       'provisioned_capacity_gb': actual_prov,
613                       'max_over_subscription_ratio': self.provisioning_factor,
614                       'thin_provisioning_support': True,
615                       'thick_provisioning_support': False,
616                       'reserved_percentage':
617                       self.configuration.reserved_percentage,
618                       'QoS_support': False,
619                       'multiattach': False,
620                       }
621        self._stats.update(self.client.get_extra_capabilities())
622
623    def get_volume_stats(self, refresh=False):
624        """Get volume stats.
625
626        If 'refresh' is True, run update the stats first.
627        """
628        if refresh:
629            self._update_volume_stats()
630        return self._stats
631
632    def manage_existing(self, volume, existing_ref, is_snapshot=False):
633        """Manages an existing LV."""
634        lv_name = existing_ref['source-name']
635        # Attempt to locate the volume.
636        try:
637            vol_obj = self.client.req('volumes', name=lv_name)['content']
638            if (
639                is_snapshot and
640                (not vol_obj['ancestor-vol-id'] or
641                 vol_obj['ancestor-vol-id'][XTREMIO_OID_NAME] !=
642                 volume.volume_id)):
643                kwargs = {'existing_ref': lv_name,
644                          'reason': 'Not a snapshot of vol %s' %
645                          volume.volume_id}
646                raise exception.ManageExistingInvalidReference(**kwargs)
647        except exception.NotFound:
648            kwargs = {'existing_ref': lv_name,
649                      'reason': 'Specified logical %s does not exist.' %
650                      'snapshot' if is_snapshot else 'volume'}
651            raise exception.ManageExistingInvalidReference(**kwargs)
652
653        # Attempt to rename the LV to match the OpenStack internal name.
654        self.client.req('volumes', 'PUT', data={'vol-name': volume['id']},
655                        idx=vol_obj['index'])
656
657    def manage_existing_get_size(self, volume, existing_ref,
658                                 is_snapshot=False):
659        """Return size of an existing LV for manage_existing."""
660        # Check that the reference is valid
661        if 'source-name' not in existing_ref:
662            reason = _('Reference must contain source-name element.')
663            raise exception.ManageExistingInvalidReference(
664                existing_ref=existing_ref, reason=reason)
665        lv_name = existing_ref['source-name']
666        # Attempt to locate the volume.
667        try:
668            vol_obj = self.client.req('volumes', name=lv_name)['content']
669        except exception.NotFound:
670            kwargs = {'existing_ref': lv_name,
671                      'reason': 'Specified logical %s does not exist.' %
672                      'snapshot' if is_snapshot else 'volume'}
673            raise exception.ManageExistingInvalidReference(**kwargs)
674        # LV size is returned in gigabytes.  Attempt to parse size as a float
675        # and round up to the next integer.
676        lv_size = int(math.ceil(float(vol_obj['vol-size']) / units.Mi))
677
678        return lv_size
679
680    def unmanage(self, volume, is_snapshot=False):
681        """Removes the specified volume from Cinder management."""
682        # trying to rename the volume to [cinder name]-unmanged
683        try:
684            self.client.req('volumes', 'PUT', name=volume['id'],
685                            data={'vol-name': volume['name'] + '-unmanged'})
686        except exception.NotFound:
687            LOG.info("%(typ)s with the name %(name)s wasn't found, "
688                     "can't unmanage",
689                     {'typ': 'Snapshot' if is_snapshot else 'Volume',
690                      'name': volume['id']})
691            raise exception.VolumeNotFound(volume_id=volume['id'])
692
693    def manage_existing_snapshot(self, snapshot, existing_ref):
694        self.manage_existing(snapshot, existing_ref, True)
695
696    def manage_existing_snapshot_get_size(self, snapshot, existing_ref):
697        return self.manage_existing_get_size(snapshot, existing_ref, True)
698
699    def unmanage_snapshot(self, snapshot):
700        self.unmanage(snapshot, True)
701
702    def extend_volume(self, volume, new_size):
703        """Extend an existing volume's size."""
704        data = {'vol-size': six.text_type(new_size) + 'g'}
705        try:
706            self.client.req('volumes', 'PUT', data, name=volume['id'])
707        except exception.NotFound:
708            msg = _("can't find the volume to extend")
709            raise exception.VolumeDriverException(message=msg)
710
711    def check_for_export(self, context, volume_id):
712        """Make sure volume is exported."""
713        pass
714
715    def terminate_connection(self, volume, connector, **kwargs):
716        """Disallow connection from connector"""
717        tg_index = '1'
718
719        if not connector:
720            vol = self.client.req('volumes', name=volume.id)['content']
721            # foce detach, unmap all IGs from volume
722            IG_OID = 0
723            ig_indexes = [lun_map[IG_OID][XTREMIO_OID_INDEX] for
724                          lun_map in vol['lun-mapping-list']]
725            LOG.info('Force detach volume %(vol)s from luns %(luns)s.',
726                     {'vol': vol['name'], 'luns': ig_indexes})
727        else:
728            vol = self.client.req('volumes', name=volume.id,
729                                  data={'prop': 'index'})['content']
730            ig_indexes = self._get_ig_indexes_from_initiators(connector)
731
732        for ig_idx in ig_indexes:
733            lm_name = '%s_%s_%s' % (six.text_type(vol['index']),
734                                    six.text_type(ig_idx),
735                                    tg_index)
736            LOG.debug('Removing lun map %s.', lm_name)
737            try:
738                self.client.req('lun-maps', 'DELETE', name=lm_name)
739            except exception.NotFound:
740                LOG.warning("terminate_connection: lun map not found")
741
742        if self.clean_ig:
743            for idx in ig_indexes:
744                try:
745                    ig = self.client.req('initiator-groups', 'GET',
746                                         {'prop': 'num-of-vols'},
747                                         idx=idx)['content']
748                    if ig['num-of-vols'] == 0:
749                        self.client.req('initiator-groups', 'DELETE', idx=idx)
750                except (exception.NotFound,
751                        exception.VolumeBackendAPIException):
752                    LOG.warning('Failed to clean IG %d without mappings', idx)
753
754    def _get_password(self):
755        return ''.join(RANDOM.choice
756                       (string.ascii_uppercase + string.digits)
757                       for _ in range(12))
758
759    def create_lun_map(self, volume, ig, lun_num=None):
760        try:
761            data = {'ig-id': ig, 'vol-id': volume['id']}
762            if lun_num:
763                data['lun'] = lun_num
764            res = self.client.req('lun-maps', 'POST', data)
765
766            lunmap = self._obj_from_result(res)
767            LOG.info('Created lun-map:\n%s', lunmap)
768        except exception.XtremIOAlreadyMappedError:
769            LOG.info('Volume already mapped, retrieving %(ig)s, %(vol)s',
770                     {'ig': ig, 'vol': volume['id']})
771            lunmap = self.client.find_lunmap(ig, volume['id'])
772        return lunmap
773
774    def _get_ig_name(self, connector):
775        raise NotImplementedError()
776
777    def _get_ig_indexes_from_initiators(self, connector):
778        initiator_names = self._get_initiator_names(connector)
779        return self.client.get_initiators_igs(initiator_names)
780
781    def _get_initiator_names(self, connector):
782        raise NotImplementedError()
783
784    def create_consistencygroup(self, context, group):
785        """Creates a consistency group.
786
787        :param context: the context
788        :param group: the group object to be created
789        :returns: dict -- modelUpdate = {'status': 'available'}
790        :raises: VolumeBackendAPIException
791        """
792        create_data = {'consistency-group-name': group['id']}
793        self.client.req('consistency-groups', 'POST', data=create_data,
794                        ver='v2')
795        return {'status': fields.ConsistencyGroupStatus.AVAILABLE}
796
797    def delete_consistencygroup(self, context, group, volumes):
798        """Deletes a consistency group."""
799        self.client.req('consistency-groups', 'DELETE', name=group['id'],
800                        ver='v2')
801
802        volumes_model_update = []
803
804        for volume in volumes:
805            self.delete_volume(volume)
806
807            update_item = {'id': volume['id'],
808                           'status': 'deleted'}
809
810            volumes_model_update.append(update_item)
811
812        model_update = {'status': group['status']}
813
814        return model_update, volumes_model_update
815
816    def _get_snapset_ancestors(self, snapset_name):
817        snapset = self.client.req('snapshot-sets',
818                                  name=snapset_name)['content']
819        volume_ids = [s[XTREMIO_OID_INDEX] for s in snapset['vol-list']]
820        return {v['ancestor-vol-id'][XTREMIO_OID_NAME]: v['name'] for v
821                in self.client.req('volumes',
822                                   data={'full': 1,
823                                         'props':
824                                         'ancestor-vol-id'})['volumes']
825                if v['index'] in volume_ids}
826
827    def create_consistencygroup_from_src(self, context, group, volumes,
828                                         cgsnapshot=None, snapshots=None,
829                                         source_cg=None, source_vols=None):
830        """Creates a consistencygroup from source.
831
832        :param context: the context of the caller.
833        :param group: the dictionary of the consistency group to be created.
834        :param volumes: a list of volume dictionaries in the group.
835        :param cgsnapshot: the dictionary of the cgsnapshot as source.
836        :param snapshots: a list of snapshot dictionaries in the cgsnapshot.
837        :param source_cg: the dictionary of a consistency group as source.
838        :param source_vols: a list of volume dictionaries in the source_cg.
839        :returns: model_update, volumes_model_update
840        """
841        if not (cgsnapshot and snapshots and not source_cg or
842                source_cg and source_vols and not cgsnapshot):
843            msg = _("create_consistencygroup_from_src only supports a "
844                    "cgsnapshot source or a consistency group source. "
845                    "Multiple sources cannot be used.")
846            raise exception.InvalidInput(msg)
847
848        if cgsnapshot:
849            snap_name = self._get_cgsnap_name(cgsnapshot)
850            snap_by_anc = self._get_snapset_ancestors(snap_name)
851            for volume, snapshot in zip(volumes, snapshots):
852                real_snap = snap_by_anc[snapshot['volume_id']]
853                self.create_volume_from_snapshot(
854                    volume,
855                    {'id': real_snap,
856                     'volume_size': snapshot['volume_size']})
857
858        elif source_cg:
859            data = {'consistency-group-id': source_cg['id'],
860                    'snapshot-set-name': group['id']}
861            self.client.req('snapshots', 'POST', data, ver='v2')
862            snap_by_anc = self._get_snapset_ancestors(group['id'])
863            for volume, src_vol in zip(volumes, source_vols):
864                snap_vol_name = snap_by_anc[src_vol['id']]
865                self.client.req('volumes', 'PUT', {'name': volume['id']},
866                                name=snap_vol_name)
867
868        create_data = {'consistency-group-name': group['id'],
869                       'vol-list': [v['id'] for v in volumes]}
870        self.client.req('consistency-groups', 'POST', data=create_data,
871                        ver='v2')
872
873        return None, None
874
875    def update_consistencygroup(self, context, group,
876                                add_volumes=None, remove_volumes=None):
877        """Updates a consistency group.
878
879        :param context: the context of the caller.
880        :param group: the dictionary of the consistency group to be updated.
881        :param add_volumes: a list of volume dictionaries to be added.
882        :param remove_volumes: a list of volume dictionaries to be removed.
883        :returns: model_update, add_volumes_update, remove_volumes_update
884        """
885        add_volumes = add_volumes if add_volumes else []
886        remove_volumes = remove_volumes if remove_volumes else []
887        for vol in add_volumes:
888            add_data = {'vol-id': vol['id'], 'cg-id': group['id']}
889            self.client.req('consistency-group-volumes', 'POST', add_data,
890                            ver='v2')
891        for vol in remove_volumes:
892            remove_data = {'vol-id': vol['id'], 'cg-id': group['id']}
893            self.client.req('consistency-group-volumes', 'DELETE', remove_data,
894                            name=group['id'], ver='v2')
895        return None, None, None
896
897    def _get_cgsnap_name(self, cgsnapshot):
898
899        group_id = cgsnapshot.get('group_id')
900        if group_id is None:
901            group_id = cgsnapshot.get('consistencygroup_id')
902
903        return '%(cg)s%(snap)s' % {'cg': group_id
904                                   .replace('-', ''),
905                                   'snap': cgsnapshot['id'].replace('-', '')}
906
907    def create_cgsnapshot(self, context, cgsnapshot, snapshots):
908        """Creates a cgsnapshot."""
909
910        group_id = cgsnapshot.get('group_id')
911        if group_id is None:
912            group_id = cgsnapshot.get('consistencygroup_id')
913
914        data = {'consistency-group-id': group_id,
915                'snapshot-set-name': self._get_cgsnap_name(cgsnapshot)}
916        self.client.req('snapshots', 'POST', data, ver='v2')
917
918        return None, None
919
920    def delete_cgsnapshot(self, context, cgsnapshot, snapshots):
921        """Deletes a cgsnapshot."""
922        self.client.req('snapshot-sets', 'DELETE',
923                        name=self._get_cgsnap_name(cgsnapshot), ver='v2')
924        return None, None
925
926    def create_group(self, context, group):
927        """Creates a group.
928
929        :param context: the context of the caller.
930        :param group: the group object.
931        :returns: model_update
932        """
933
934        # the driver treats a group as a CG internally.
935        # We proxy the calls to the CG api.
936        return self.create_consistencygroup(context, group)
937
938    def delete_group(self, context, group, volumes):
939        """Deletes a group.
940
941        :param context: the context of the caller.
942        :param group: the group object.
943        :param volumes: a list of volume objects in the group.
944        :returns: model_update, volumes_model_update
945        """
946
947        # the driver treats a group as a CG internally.
948        # We proxy the calls to the CG api.
949        return self.delete_consistencygroup(context, group, volumes)
950
951    def update_group(self, context, group,
952                     add_volumes=None, remove_volumes=None):
953        """Updates a group.
954
955        :param context: the context of the caller.
956        :param group: the group object.
957        :param add_volumes: a list of volume objects to be added.
958        :param remove_volumes: a list of volume objects to be removed.
959        :returns: model_update, add_volumes_update, remove_volumes_update
960        """
961
962        # the driver treats a group as a CG internally.
963        # We proxy the calls to the CG api.
964        return self.update_consistencygroup(context, group, add_volumes,
965                                            remove_volumes)
966
967    def create_group_from_src(self, context, group, volumes,
968                              group_snapshot=None, snapshots=None,
969                              source_group=None, source_vols=None):
970        """Creates a group from source.
971
972        :param context: the context of the caller.
973        :param group: the Group object to be created.
974        :param volumes: a list of Volume objects in the group.
975        :param group_snapshot: the GroupSnapshot object as source.
976        :param snapshots: a list of snapshot objects in group_snapshot.
977        :param source_group: the Group object as source.
978        :param source_vols: a list of volume objects in the source_group.
979        :returns: model_update, volumes_model_update
980        """
981
982        # the driver treats a group as a CG internally.
983        # We proxy the calls to the CG api.
984        return self.create_consistencygroup_from_src(context, group, volumes,
985                                                     group_snapshot, snapshots,
986                                                     source_group, source_vols)
987
988    def create_group_snapshot(self, context, group_snapshot, snapshots):
989        """Creates a group_snapshot.
990
991        :param context: the context of the caller.
992        :param group_snapshot: the GroupSnapshot object to be created.
993        :param snapshots: a list of Snapshot objects in the group_snapshot.
994        :returns: model_update, snapshots_model_update
995        """
996
997        # the driver treats a group as a CG internally.
998        # We proxy the calls to the CG api.
999        return self.create_cgsnapshot(context, group_snapshot, snapshots)
1000
1001    def delete_group_snapshot(self, context, group_snapshot, snapshots):
1002        """Deletes a group_snapshot.
1003
1004        :param context: the context of the caller.
1005        :param group_snapshot: the GroupSnapshot object to be deleted.
1006        :param snapshots: a list of snapshot objects in the group_snapshot.
1007        :returns: model_update, snapshots_model_update
1008        """
1009
1010        # the driver treats a group as a CG internally.
1011        # We proxy the calls to the CG api.
1012        return self.delete_cgsnapshot(context, group_snapshot, snapshots)
1013
1014    def _get_ig(self, name):
1015        try:
1016            return self.client.req('initiator-groups', 'GET',
1017                                   name=name)['content']
1018        except exception.NotFound:
1019            pass
1020
1021    def _create_ig(self, name):
1022        # create an initiator group to hold the initiator
1023        data = {'ig-name': name}
1024        self.client.req('initiator-groups', 'POST', data)
1025        try:
1026            return self.client.req('initiator-groups', name=name)['content']
1027        except exception.NotFound:
1028            raise (exception.VolumeBackendAPIException
1029                   (data=_("Failed to create IG, %s") % name))
1030
1031
1032@interface.volumedriver
1033class XtremIOISCSIDriver(XtremIOVolumeDriver, driver.ISCSIDriver):
1034    """Executes commands relating to ISCSI volumes.
1035
1036    We make use of model provider properties as follows:
1037
1038    ``provider_location``
1039      if present, contains the iSCSI target information in the same
1040      format as an ietadm discovery
1041      i.e. '<ip>:<port>,<portal> <target IQN>'
1042
1043    ``provider_auth``
1044      if present, contains a space-separated triple:
1045      '<auth method> <auth username> <auth password>'.
1046      `CHAP` is the only auth_method in use at the moment.
1047    """
1048    driver_name = 'XtremIO_ISCSI'
1049
1050    def __init__(self, *args, **kwargs):
1051        super(XtremIOISCSIDriver, self).__init__(*args, **kwargs)
1052        self.protocol = 'iSCSI'
1053
1054    def _add_auth(self, data, login_chap, discovery_chap):
1055        login_passwd, discovery_passwd = None, None
1056        if login_chap:
1057            data['initiator-authentication-user-name'] = 'chap_user'
1058            login_passwd = self._get_password()
1059            data['initiator-authentication-password'] = login_passwd
1060        if discovery_chap:
1061            data['initiator-discovery-user-name'] = 'chap_user'
1062            discovery_passwd = self._get_password()
1063            data['initiator-discovery-password'] = discovery_passwd
1064        return login_passwd, discovery_passwd
1065
1066    def _create_initiator(self, connector, login_chap, discovery_chap):
1067        initiator = self._get_initiator_names(connector)[0]
1068        # create an initiator
1069        data = {'initiator-name': initiator,
1070                'ig-id': initiator,
1071                'port-address': initiator}
1072        l, d = self._add_auth(data, login_chap, discovery_chap)
1073        self.client.req('initiators', 'POST', data)
1074        return l, d
1075
1076    def initialize_connection(self, volume, connector):
1077        try:
1078            sys = self.client.get_cluster()
1079        except exception.NotFound:
1080            msg = _("XtremIO not initialized correctly, no clusters found")
1081            raise exception.VolumeBackendAPIException(data=msg)
1082        login_chap = (sys.get('chap-authentication-mode', 'disabled') !=
1083                      'disabled')
1084        discovery_chap = (sys.get('chap-discovery-mode', 'disabled') !=
1085                          'disabled')
1086        initiator_name = self._get_initiator_names(connector)[0]
1087        initiator = self.client.get_initiator(initiator_name)
1088        if initiator:
1089            login_passwd = initiator['chap-authentication-initiator-password']
1090            discovery_passwd = initiator['chap-discovery-initiator-password']
1091            ig = self._get_ig(initiator['ig-id'][XTREMIO_OID_NAME])
1092        else:
1093            ig = self._get_ig(self._get_ig_name(connector))
1094            if not ig:
1095                ig = self._create_ig(self._get_ig_name(connector))
1096            (login_passwd,
1097             discovery_passwd) = self._create_initiator(connector,
1098                                                        login_chap,
1099                                                        discovery_chap)
1100        # if CHAP was enabled after the initiator was created
1101        if login_chap and not login_passwd:
1102            LOG.info('Initiator has no password while using chap, adding it.')
1103            data = {}
1104            (login_passwd,
1105             d_passwd) = self._add_auth(data, login_chap, discovery_chap and
1106                                        not discovery_passwd)
1107            discovery_passwd = (discovery_passwd if discovery_passwd
1108                                else d_passwd)
1109            self.client.req('initiators', 'PUT', data, idx=initiator['index'])
1110
1111        # lun mappping
1112        lunmap = self.create_lun_map(volume, ig['ig-id'][XTREMIO_OID_NAME])
1113
1114        properties = self._get_iscsi_properties(lunmap)
1115
1116        if login_chap:
1117            properties['auth_method'] = 'CHAP'
1118            properties['auth_username'] = 'chap_user'
1119            properties['auth_password'] = login_passwd
1120        if discovery_chap:
1121            properties['discovery_auth_method'] = 'CHAP'
1122            properties['discovery_auth_username'] = 'chap_user'
1123            properties['discovery_auth_password'] = discovery_passwd
1124        LOG.debug('init conn params:\n%s',
1125                  strutils.mask_dict_password(properties))
1126        return {
1127            'driver_volume_type': 'iscsi',
1128            'data': properties
1129        }
1130
1131    def _get_iscsi_properties(self, lunmap):
1132        """Gets iscsi configuration.
1133
1134        :target_discovered:    boolean indicating whether discovery was used
1135        :target_iqn:    the IQN of the iSCSI target
1136        :target_portal:    the portal of the iSCSI target
1137        :target_lun:    the lun of the iSCSI target
1138        :volume_id:    the id of the volume (currently used by xen)
1139        :auth_method:, :auth_username:, :auth_password:
1140            the authentication details. Right now, either auth_method is not
1141            present meaning no authentication, or auth_method == `CHAP`
1142            meaning use CHAP with the specified credentials.
1143        multiple connection return
1144        :target_iqns, :target_portals, :target_luns, which contain lists of
1145        multiple values. The main portal information is also returned in
1146        :target_iqn, :target_portal, :target_lun for backward compatibility.
1147        """
1148        portals = self.client.get_iscsi_portals()
1149        if not portals:
1150            msg = _("XtremIO not configured correctly, no iscsi portals found")
1151            LOG.error(msg)
1152            raise exception.VolumeDriverException(message=msg)
1153        portal = RANDOM.choice(portals)
1154        portal_addr = ('%(ip)s:%(port)d' %
1155                       {'ip': portal['ip-addr'].split('/')[0],
1156                        'port': portal['ip-port']})
1157
1158        tg_portals = ['%(ip)s:%(port)d' % {'ip': p['ip-addr'].split('/')[0],
1159                                           'port': p['ip-port']}
1160                      for p in portals]
1161        properties = {'target_discovered': False,
1162                      'target_iqn': portal['port-address'],
1163                      'target_lun': lunmap['lun'],
1164                      'target_portal': portal_addr,
1165                      'target_iqns': [p['port-address'] for p in portals],
1166                      'target_portals': tg_portals,
1167                      'target_luns': [lunmap['lun']] * len(portals)}
1168        return properties
1169
1170    def _get_initiator_names(self, connector):
1171        return [connector['initiator']]
1172
1173    def _get_ig_name(self, connector):
1174        return connector['initiator']
1175
1176
1177@interface.volumedriver
1178class XtremIOFCDriver(XtremIOVolumeDriver,
1179                      driver.FibreChannelDriver):
1180
1181    def __init__(self, *args, **kwargs):
1182        super(XtremIOFCDriver, self).__init__(*args, **kwargs)
1183        self.protocol = 'FC'
1184        self._targets = None
1185
1186    def get_targets(self):
1187        if not self._targets:
1188            try:
1189                targets = self.client.get_fc_up_ports()
1190                self._targets = [target['port-address'].replace(':', '')
1191                                 for target in targets]
1192            except exception.NotFound:
1193                raise (exception.VolumeBackendAPIException
1194                       (data=_("Failed to get targets")))
1195        return self._targets
1196
1197    def _get_free_lun(self, igs):
1198        luns = []
1199        for ig in igs:
1200            luns.extend(lm['lun'] for lm in
1201                        self.client.req('lun-maps',
1202                                        data={'full': 1, 'prop': 'lun',
1203                                              'filter': 'ig-name:eq:%s' % ig})
1204                        ['lun-maps'])
1205        uniq_luns = set(luns + [0])
1206        seq = range(len(uniq_luns) + 1)
1207        return min(set(seq) - uniq_luns)
1208
1209    @fczm_utils.add_fc_zone
1210    def initialize_connection(self, volume, connector):
1211        wwpns = self._get_initiator_names(connector)
1212        ig_name = self._get_ig_name(connector)
1213        i_t_map = {}
1214        found = []
1215        new = []
1216        for wwpn in wwpns:
1217            init = self.client.get_initiator(wwpn)
1218            if init:
1219                found.append(init)
1220            else:
1221                new.append(wwpn)
1222            i_t_map[wwpn.replace(':', '')] = self.get_targets()
1223        # get or create initiator group
1224        if new:
1225            ig = self._get_ig(ig_name)
1226            if not ig:
1227                ig = self._create_ig(ig_name)
1228            for wwpn in new:
1229                data = {'initiator-name': wwpn, 'ig-id': ig_name,
1230                        'port-address': wwpn}
1231                self.client.req('initiators', 'POST', data)
1232        igs = list(set([i['ig-id'][XTREMIO_OID_NAME] for i in found]))
1233        if new and ig['ig-id'][XTREMIO_OID_NAME] not in igs:
1234            igs.append(ig['ig-id'][XTREMIO_OID_NAME])
1235
1236        if len(igs) > 1:
1237            lun_num = self._get_free_lun(igs)
1238        else:
1239            lun_num = None
1240        for ig in igs:
1241            lunmap = self.create_lun_map(volume, ig, lun_num)
1242            lun_num = lunmap['lun']
1243        return {'driver_volume_type': 'fibre_channel',
1244                'data': {
1245                    'target_discovered': False,
1246                    'target_lun': lun_num,
1247                    'target_wwn': self.get_targets(),
1248                    'initiator_target_map': i_t_map}}
1249
1250    @fczm_utils.remove_fc_zone
1251    def terminate_connection(self, volume, connector, **kwargs):
1252        (super(XtremIOFCDriver, self)
1253         .terminate_connection(volume, connector, **kwargs))
1254        has_volumes = (not connector
1255                       or self.client.
1256                       num_of_mapped_volumes(self._get_ig_name(connector)) > 0)
1257
1258        if has_volumes:
1259            data = {}
1260        else:
1261            i_t_map = {}
1262            for initiator in self._get_initiator_names(connector):
1263                i_t_map[initiator.replace(':', '')] = self.get_targets()
1264            data = {'target_wwn': self.get_targets(),
1265                    'initiator_target_map': i_t_map}
1266
1267        return {'driver_volume_type': 'fibre_channel',
1268                'data': data}
1269
1270    def _get_initiator_names(self, connector):
1271        return [wwpn if ':' in wwpn else
1272                ':'.join(wwpn[i:i + 2] for i in range(0, len(wwpn), 2))
1273                for wwpn in connector['wwpns']]
1274
1275    def _get_ig_name(self, connector):
1276        return connector['host']
1277