# Copyright (c) 2018 Dell Inc. or its subsidiaries. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Driver for Dell EMC XtremIO Storage. Supports XtremIO version 2.4 and up. .. code-block:: none 1.0.0 - initial release 1.0.1 - enable volume extend 1.0.2 - added FC support, improved error handling 1.0.3 - update logging level, add translation 1.0.4 - support for FC zones 1.0.5 - add support for XtremIO 4.0 1.0.6 - add support for iSCSI multipath, CA validation, consistency groups, R/O snapshots, CHAP discovery authentication 1.0.7 - cache glance images on the array 1.0.8 - support for volume retype, CG fixes 1.0.9 - performance improvements, support force detach, support for X2 1.0.10 - option to clean unused IGs 1.0.11 - add support for multiattach """ import json import math import random 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 requests 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 volume_utils from cinder.zonemanager import utils as fczm_utils LOG = logging.getLogger(__name__) CONF = cfg.CONF XTREMIO_OPTS = [ cfg.StrOpt('xtremio_cluster_name', default='', help='XMS cluster id in multi-cluster environment'), cfg.IntOpt('xtremio_array_busy_retry_count', default=5, help='Number of retries in case array is busy'), cfg.IntOpt('xtremio_array_busy_retry_interval', default=5, help='Interval between retries in case array is busy'), cfg.IntOpt('xtremio_volumes_per_glance_cache', default=100, help='Number of volumes created from each cached glance image'), cfg.BoolOpt('xtremio_clean_unused_ig', default=False, help='Should the driver remove initiator groups with no ' 'volumes after the last connection was terminated. ' 'Since the behavior till now was to leave ' 'the IG be, we default to False (not deleting IGs ' 'without connected volumes); setting this parameter ' 'to True will remove any IG after terminating its ' 'connection to the last volume.')] CONF.register_opts(XTREMIO_OPTS, group=configuration.SHARED_CONF_GROUP) RANDOM = random.Random() OBJ_NOT_FOUND_ERR = 'obj_not_found' VOL_NOT_UNIQUE_ERR = 'vol_obj_name_not_unique' VOL_OBJ_NOT_FOUND_ERR = 'vol_obj_not_found' ALREADY_MAPPED_ERR = 'already_mapped' SYSTEM_BUSY = 'system_is_busy' TOO_MANY_OBJECTS = 'too_many_objs' TOO_MANY_SNAPSHOTS_PER_VOL = 'too_many_snapshots_per_vol' XTREMIO_OID_NAME = 1 XTREMIO_OID_INDEX = 2 class XtremIOAlreadyMappedError(exception.VolumeDriverException): message = _("Volume to Initiator Group mapping already exists") class XtremIOArrayBusy(exception.VolumeDriverException): message = _("System is busy, retry operation.") class XtremIOSnapshotsLimitExceeded(exception.VolumeDriverException): message = _("Exceeded the limit of snapshots per volume") class XtremIOClient(object): def __init__(self, configuration, cluster_id): self.configuration = configuration self.cluster_id = cluster_id self.verify = (self.configuration. safe_get('driver_ssl_cert_verify') or False) if self.verify: verify_path = (self.configuration. safe_get('driver_ssl_cert_path') or None) if verify_path: self.verify = verify_path def get_base_url(self, ver): if ver == 'v1': return 'https://%s/api/json/types' % self.configuration.san_ip elif ver == 'v2': return 'https://%s/api/json/v2/types' % self.configuration.san_ip def req(self, object_type='volumes', method='GET', data=None, name=None, idx=None, ver='v1'): @utils.retry(XtremIOArrayBusy, self.configuration.xtremio_array_busy_retry_count, self.configuration.xtremio_array_busy_retry_interval, 1) def _do_req(object_type, method, data, name, idx, ver): if not data: data = {} if name and idx: msg = _("can't handle both name and index in req") LOG.error(msg) raise exception.VolumeDriverException(message=msg) url = '%s/%s' % (self.get_base_url(ver), object_type) params = {} key = None if name: params['name'] = name key = name elif idx: url = '%s/%d' % (url, idx) key = str(idx) if method in ('GET', 'DELETE'): params.update(data) self.update_url(params, self.cluster_id) if method != 'GET': self.update_data(data, self.cluster_id) # data may include chap password LOG.debug('data: %s', strutils.mask_password(data)) LOG.debug('%(type)s %(url)s', {'type': method, 'url': url}) try: response = requests.request( method, url, params=params, data=json.dumps(data), verify=self.verify, auth=(self.configuration.san_login, self.configuration.san_password)) except requests.exceptions.RequestException as exc: msg = (_('Exception: %s') % 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 XtremIOAlreadyMappedError() elif err_msg == SYSTEM_BUSY: raise XtremIOArrayBusy() elif err_msg in (TOO_MANY_OBJECTS, TOO_MANY_SNAPSHOTS_PER_VOL): raise XtremIOSnapshotsLimitExceeded() msg = _('Bad response from XMS, %s') % response.text LOG.error(msg) raise exception.VolumeBackendAPIException(message=msg) def update_url(self, data, cluster_id): return def update_data(self, data, cluster_id): return def get_cluster(self): return self.req('clusters', idx=1)['content'] def create_snapshot(self, src, dest, ro=False): """Create a snapshot of a volume on the array. XtreamIO array snapshots are also volumes. :src: name of the source volume to be cloned :dest: name for the new snapshot :ro: new snapshot type ro/regular. only applicable to Client4 """ raise NotImplementedError() def get_extra_capabilities(self): return {} def get_initiator(self, port_address): raise NotImplementedError() def add_vol_to_cg(self, vol_id, cg_id): pass def get_initiators_igs(self, port_addresses): ig_indexes = set() for port_address in port_addresses: initiator = self.get_initiator(port_address) ig_indexes.add(initiator['ig-id'][XTREMIO_OID_INDEX]) return list(ig_indexes) def get_fc_up_ports(self): targets = [self.req('targets', name=target['name'])['content'] for target in self.req('targets')['targets']] return [target for target in targets if target['port-type'] == 'fc' and target["port-state"] == 'up'] class XtremIOClient3(XtremIOClient): def __init__(self, configuration, cluster_id): super(XtremIOClient3, self).__init__(configuration, cluster_id) self._portals = [] def find_lunmap(self, ig_name, vol_name): try: lun_mappings = self.req('lun-maps')['lun-maps'] except exception.NotFound: raise (exception.VolumeDriverException (_("can't find lun-map, ig:%(ig)s vol:%(vol)s") % {'ig': ig_name, 'vol': vol_name})) for lm_link in lun_mappings: idx = lm_link['href'].split('/')[-1] # NOTE(geguileo): There can be races so mapped elements retrieved # in the listing may no longer exist. try: lm = self.req('lun-maps', idx=int(idx))['content'] except exception.NotFound: continue if lm['ig-name'] == ig_name and lm['vol-name'] == vol_name: return lm return None def num_of_mapped_volumes(self, initiator): cnt = 0 for lm_link in self.req('lun-maps')['lun-maps']: idx = lm_link['href'].split('/')[-1] # NOTE(geguileo): There can be races so mapped elements retrieved # in the listing may no longer exist. try: lm = self.req('lun-maps', idx=int(idx))['content'] except exception.NotFound: continue if lm['ig-name'] == initiator: cnt += 1 return cnt def get_iscsi_portals(self): if self._portals: return self._portals iscsi_portals = [t['name'] for t in self.req('iscsi-portals') ['iscsi-portals']] for portal_name in iscsi_portals: try: self._portals.append(self.req('iscsi-portals', name=portal_name)['content']) except exception.NotFound: raise (exception.VolumeBackendAPIException (data=_("iscsi portal, %s, not found") % portal_name)) return self._portals def create_snapshot(self, src, dest, ro=False): data = {'snap-vol-name': dest, 'ancestor-vol-id': src} self.req('snapshots', 'POST', data) def get_initiator(self, port_address): try: return self.req('initiators', 'GET', name=port_address)['content'] except exception.NotFound: pass class XtremIOClient4(XtremIOClient): def __init__(self, configuration, cluster_id): super(XtremIOClient4, self).__init__(configuration, cluster_id) self._cluster_name = None def req(self, object_type='volumes', method='GET', data=None, name=None, idx=None, ver='v2'): return super(XtremIOClient4, self).req(object_type, method, data, name, idx, ver) def get_extra_capabilities(self): return {'consistencygroup_support': True} def find_lunmap(self, ig_name, vol_name): try: return (self.req('lun-maps', data={'full': 1, 'filter': ['vol-name:eq:%s' % vol_name, 'ig-name:eq:%s' % ig_name]}) ['lun-maps'][0]) except (KeyError, IndexError): raise exception.VolumeNotFound(volume_id=vol_name) def num_of_mapped_volumes(self, initiator): return len(self.req('lun-maps', data={'filter': 'ig-name:eq:%s' % initiator}) ['lun-maps']) def update_url(self, data, cluster_id): if cluster_id: data['cluster-name'] = cluster_id def update_data(self, data, cluster_id): if cluster_id: data['cluster-id'] = cluster_id def get_iscsi_portals(self): return self.req('iscsi-portals', data={'full': 1})['iscsi-portals'] def get_cluster(self): if not self.cluster_id: self.cluster_id = self.req('clusters')['clusters'][0]['name'] return self.req('clusters', name=self.cluster_id)['content'] def create_snapshot(self, src, dest, ro=False): data = {'snapshot-set-name': dest, 'snap-suffix': dest, 'volume-list': [src], 'snapshot-type': 'readonly' if ro else 'regular'} res = self.req('snapshots', 'POST', data, ver='v2') typ, idx = res['links'][0]['href'].split('/')[-2:] # rename the snapshot data = {'name': dest} try: self.req(typ, 'PUT', data, idx=int(idx)) except exception.VolumeBackendAPIException: # reverting LOG.error('Failed to rename the created snapshot, reverting.') self.req(typ, 'DELETE', idx=int(idx)) raise def add_vol_to_cg(self, vol_id, cg_id): add_data = {'vol-id': vol_id, 'cg-id': cg_id} self.req('consistency-group-volumes', 'POST', add_data, ver='v2') def get_initiator(self, port_address): inits = self.req('initiators', data={'filter': 'port-address:eq:' + port_address, 'full': 1})['initiators'] if len(inits) == 1: return inits[0] else: pass def get_fc_up_ports(self): return self.req('targets', data={'full': 1, 'filter': ['port-type:eq:fc', 'port-state:eq:up'], 'prop': 'port-address'})["targets"] class XtremIOClient42(XtremIOClient4): def get_initiators_igs(self, port_addresses): init_filter = ','.join('port-address:eq:{}'.format(port_address) for port_address in port_addresses) initiators = self.req('initiators', data={'filter': init_filter, 'full': 1, 'prop': 'ig-id'})['initiators'] return list(set(ig_id['ig-id'][XTREMIO_OID_INDEX] for ig_id in initiators)) class XtremIOVolumeDriver(san.SanDriver): """Executes commands relating to Volumes.""" VERSION = '1.0.11' # 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 = \ volume_utils.get_max_over_subscription_ratio( self.configuration.max_over_subscription_ratio, supports_auto=False) self.clean_ig = (self.configuration.safe_get('xtremio_clean_unused_ig') or False) self._stats = {} self.client = XtremIOClient3(self.configuration, self.cluster_id) @classmethod def get_driver_options(cls): additional_opts = cls._get_oslo_driver_opts( 'san_ip', 'san_login', 'san_password', 'driver_ssl_cert_verify', 'driver_ssl_cert_path', 'max_over_subscription_ratio', 'reserved_percentage') return XTREMIO_OPTS + additional_opts def _obj_from_result(self, res): typ, idx = res['links'][0]['href'].split('/')[-2:] return self.client.req(typ, idx=int(idx))['content'] def check_for_setup_error(self): try: name = self.client.req('clusters')['clusters'][0]['name'] cluster = self.client.req('clusters', name=name)['content'] version_text = cluster['sys-sw-version'] except exception.NotFound: msg = _("XtremIO not initialized correctly, no clusters found") raise (exception.VolumeBackendAPIException (data=msg)) ver = [int(n) for n in version_text.split('-')[0].split('.')] if ver < self.MIN_XMS_VERSION: msg = (_('Invalid XtremIO version %(cur)s,' ' version %(min)s or up is required') % {'min': self.MIN_XMS_VERSION, 'cur': ver}) LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) else: LOG.info('XtremIO Cluster version %s', version_text) client_ver = '3' if ver[0] >= 4: # get XMS version xms = self.client.req('xms', idx=1)['content'] xms_version = tuple([int(i) for i in xms['sw-version'].split('-')[0].split('.')]) LOG.info('XtremIO XMS version %s', version_text) if xms_version >= (4, 2): self.client = XtremIOClient42(self.configuration, self.cluster_id) client_ver = '4.2' else: self.client = XtremIOClient4(self.configuration, self.cluster_id) client_ver = '4' LOG.info('Using XtremIO Client %s', client_ver) def create_volume(self, volume): """Creates a volume.""" data = {'vol-name': volume['id'], 'vol-size': str(volume['size']) + 'g' } self.client.req('volumes', 'POST', data) # Add the volume to a cg in case volume requested a cgid or group_id. # If both cg_id and group_id exists in a volume. group_id will take # place. consistency_group = volume.get('consistencygroup_id') # if cg_id and group_id are both exists, we gives priority to group_id. if volume.get('group_id'): consistency_group = volume.get('group_id') if consistency_group: self.client.add_vol_to_cg(volume['id'], consistency_group) def create_volume_from_snapshot(self, volume, snapshot): """Creates a volume from a snapshot.""" if snapshot.get('cgsnapshot_id'): # get array snapshot id from CG snapshot snap_by_anc = self._get_snapset_ancestors(snapshot.cgsnapshot) snapshot_id = snap_by_anc[snapshot['volume_id']] else: snapshot_id = snapshot['id'] try: self.client.create_snapshot(snapshot_id, volume['id']) except XtremIOSnapshotsLimitExceeded as e: raise exception.CinderException(e.message) # extend the snapped volume if requested size is larger then original if volume['size'] > snapshot['volume_size']: try: self.extend_volume(volume, volume['size']) except Exception: LOG.error('failed to extend volume %s, ' 'reverting volume from snapshot operation', volume['id']) # remove the volume in case resize failed self.delete_volume(volume) raise # add new volume to consistency group if (volume.get('consistencygroup_id') and self.client is XtremIOClient4): self.client.add_vol_to_cg(volume['id'], snapshot['consistencygroup_id']) def create_cloned_volume(self, volume, src_vref): """Creates a clone of the specified volume.""" vol = self.client.req('volumes', name=src_vref['id'])['content'] ctxt = context.get_admin_context() cache = self.db.image_volume_cache_get_by_volume_id(ctxt, src_vref['id']) limit = self.configuration.safe_get('xtremio_volumes_per_glance_cache') if cache and limit and limit > 0 and limit <= vol['num-of-dest-snaps']: raise exception.SnapshotLimitReached(set_limit=limit) try: self.client.create_snapshot(src_vref['id'], volume['id']) except XtremIOSnapshotsLimitExceeded as e: raise exception.CinderException(e.message) # extend the snapped volume if requested size is larger then original if volume['size'] > src_vref['size']: try: self.extend_volume(volume, volume['size']) except Exception: LOG.error('failed to extend volume %s, ' 'reverting clone operation', volume['id']) # remove the volume in case resize failed self.delete_volume(volume) raise if volume.get('consistencygroup_id') and self.client is XtremIOClient4: self.client.add_vol_to_cg(volume['id'], volume['consistencygroup_id']) def delete_volume(self, volume): """Deletes a volume.""" try: self.client.req('volumes', 'DELETE', name=volume.name_id) except exception.NotFound: LOG.info("volume %s doesn't exist", volume.name_id) def create_snapshot(self, snapshot): """Creates a snapshot.""" self.client.create_snapshot(snapshot.volume_id, snapshot.id, True) def delete_snapshot(self, snapshot): """Deletes a snapshot.""" try: self.client.req('volumes', 'DELETE', name=snapshot.id) except exception.NotFound: LOG.info("snapshot %s doesn't exist", snapshot.id) def update_migrated_volume(self, ctxt, volume, new_volume, original_volume_status): # as the volume name is used to id the volume we need to rename it name_id = None provider_location = None current_name = new_volume['id'] original_name = volume['id'] try: data = {'name': original_name} self.client.req('volumes', 'PUT', data, name=current_name) except exception.VolumeBackendAPIException: LOG.error('Unable to rename the logical volume ' 'for volume: %s', original_name) # If the rename fails, _name_id should be set to the new # volume id and provider_location should be set to the # one from the new volume as well. name_id = new_volume['_name_id'] or new_volume['id'] provider_location = new_volume['provider_location'] return {'_name_id': name_id, 'provider_location': provider_location} def _update_volume_stats(self): sys = self.client.get_cluster() physical_space = int(sys["ud-ssd-space"]) / units.Mi used_physical_space = int(sys["ud-ssd-space-in-use"]) / units.Mi free_physical = physical_space - used_physical_space actual_prov = int(sys["vol-size"]) / units.Mi self._stats = {'volume_backend_name': self.backend_name, 'vendor_name': 'Dell EMC', 'driver_version': self.VERSION, 'storage_protocol': self.protocol, 'total_capacity_gb': physical_space, 'free_capacity_gb': free_physical, 'provisioned_capacity_gb': actual_prov, 'max_over_subscription_ratio': self.provisioning_factor, 'thin_provisioning_support': True, 'thick_provisioning_support': False, 'reserved_percentage': self.configuration.reserved_percentage, 'QoS_support': False, 'multiattach': True, } self._stats.update(self.client.get_extra_capabilities()) def manage_existing(self, volume, existing_ref, is_snapshot=False): """Manages an existing LV.""" lv_name = existing_ref['source-name'] # Attempt to locate the volume. try: vol_obj = self.client.req('volumes', name=lv_name)['content'] if ( is_snapshot and (not vol_obj['ancestor-vol-id'] or vol_obj['ancestor-vol-id'][XTREMIO_OID_NAME] != volume.volume_id)): kwargs = {'existing_ref': lv_name, 'reason': 'Not a snapshot of vol %s' % volume.volume_id} raise exception.ManageExistingInvalidReference(**kwargs) except exception.NotFound: kwargs = {'existing_ref': lv_name, 'reason': 'Specified logical %s does not exist.' % 'snapshot' if is_snapshot else 'volume'} raise exception.ManageExistingInvalidReference(**kwargs) # Attempt to rename the LV to match the OpenStack internal name. self.client.req('volumes', 'PUT', data={'vol-name': volume['id']}, idx=vol_obj['index']) def manage_existing_get_size(self, volume, existing_ref, is_snapshot=False): """Return size of an existing LV for manage_existing.""" # Check that the reference is valid if 'source-name' not in existing_ref: reason = _('Reference must contain source-name element.') raise exception.ManageExistingInvalidReference( existing_ref=existing_ref, reason=reason) lv_name = existing_ref['source-name'] # Attempt to locate the volume. try: vol_obj = self.client.req('volumes', name=lv_name)['content'] except exception.NotFound: kwargs = {'existing_ref': lv_name, 'reason': 'Specified logical %s does not exist.' % 'snapshot' if is_snapshot else 'volume'} raise exception.ManageExistingInvalidReference(**kwargs) # LV size is returned in gigabytes. Attempt to parse size as a float # and round up to the next integer. lv_size = int(math.ceil(float(vol_obj['vol-size']) / units.Mi)) return lv_size def unmanage(self, volume, is_snapshot=False): """Removes the specified volume from Cinder management.""" # trying to rename the volume to [cinder name]-unmanged try: self.client.req('volumes', 'PUT', name=volume['id'], data={'vol-name': volume['name'] + '-unmanged'}) except exception.NotFound: LOG.info("%(typ)s with the name %(name)s wasn't found, " "can't unmanage", {'typ': 'Snapshot' if is_snapshot else 'Volume', 'name': volume['id']}) raise exception.VolumeNotFound(volume_id=volume['id']) def manage_existing_snapshot(self, snapshot, existing_ref): self.manage_existing(snapshot, existing_ref, True) def manage_existing_snapshot_get_size(self, snapshot, existing_ref): return self.manage_existing_get_size(snapshot, existing_ref, True) def unmanage_snapshot(self, snapshot): self.unmanage(snapshot, True) def extend_volume(self, volume, new_size): """Extend an existing volume's size.""" data = {'vol-size': 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: host = connector['host'] attachment_list = volume.volume_attachment LOG.debug("Volume attachment list: %(atl)s. " "Attachment type: %(at)s", {'atl': attachment_list, 'at': type(attachment_list)}) try: att_list = attachment_list.objects except AttributeError: att_list = attachment_list if att_list is not None: host_list = [att.connector['host'] for att in att_list if att is not None and att.connector is not None] current_host_occurances = host_list.count(host) if current_host_occurances > 1: LOG.info("Volume is attached to multiple instances on " "this host. Not removing the lun map.") return vol = self.client.req('volumes', name=volume.id, data={'prop': 'index'})['content'] ig_indexes = self._get_ig_indexes_from_initiators(connector) for ig_idx in ig_indexes: lm_name = '%s_%s_%s' % (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 volume_utils.generate_password( length=12, symbolgroups=(string.ascii_uppercase + string.digits)) def create_lun_map(self, volume, ig, lun_num=None): try: data = {'ig-id': ig, 'vol-id': volume['id']} if lun_num: data['lun'] = lun_num res = self.client.req('lun-maps', 'POST', data) lunmap = self._obj_from_result(res) LOG.info('Created lun-map:\n%s', lunmap) except XtremIOAlreadyMappedError: LOG.info('Volume already mapped, retrieving %(ig)s, %(vol)s', {'ig': ig, 'vol': volume['id']}) lunmap = self.client.find_lunmap(ig, volume['id']) return lunmap def _get_ig_name(self, connector): raise NotImplementedError() def _get_ig_indexes_from_initiators(self, connector): initiator_names = self._get_initiator_names(connector) return self.client.get_initiators_igs(initiator_names) def _get_initiator_names(self, connector): raise NotImplementedError() def create_consistencygroup(self, context, group): """Creates a consistency group. :param context: the context :param group: the group object to be created :returns: dict -- modelUpdate = {'status': 'available'} :raises: VolumeBackendAPIException """ create_data = {'consistency-group-name': group['id']} self.client.req('consistency-groups', 'POST', data=create_data, ver='v2') return {'status': fields.ConsistencyGroupStatus.AVAILABLE} def delete_consistencygroup(self, context, group, volumes): """Deletes a consistency group.""" self.client.req('consistency-groups', 'DELETE', name=group['id'], ver='v2') volumes_model_update = [] for volume in volumes: self.delete_volume(volume) update_item = {'id': volume['id'], 'status': 'deleted'} volumes_model_update.append(update_item) model_update = {'status': group['status']} return model_update, volumes_model_update def _get_snapset_ancestors(self, snapset_name): snapset = self.client.req('snapshot-sets', name=snapset_name)['content'] volume_ids = [s[XTREMIO_OID_INDEX] for s in snapset['vol-list']] return {v['ancestor-vol-id'][XTREMIO_OID_NAME]: v['name'] for v in self.client.req('volumes', data={'full': 1, 'props': 'ancestor-vol-id'})['volumes'] if v['index'] in volume_ids} def create_consistencygroup_from_src(self, context, group, volumes, cgsnapshot=None, snapshots=None, source_cg=None, source_vols=None): """Creates a consistencygroup from source. :param context: the context of the caller. :param group: the dictionary of the consistency group to be created. :param volumes: a list of volume dictionaries in the group. :param cgsnapshot: the dictionary of the cgsnapshot as source. :param snapshots: a list of snapshot dictionaries in the cgsnapshot. :param source_cg: the dictionary of a consistency group as source. :param source_vols: a list of volume dictionaries in the source_cg. :returns: model_update, volumes_model_update """ if not (cgsnapshot and snapshots and not source_cg or source_cg and source_vols and not cgsnapshot): msg = _("create_consistencygroup_from_src only supports a " "cgsnapshot source or a consistency group source. " "Multiple sources cannot be used.") raise exception.InvalidInput(msg) if cgsnapshot: snap_name = self._get_cgsnap_name(cgsnapshot) snap_by_anc = self._get_snapset_ancestors(snap_name) for volume, snapshot in zip(volumes, snapshots): real_snap = snap_by_anc[snapshot['volume_id']] self.create_volume_from_snapshot( volume, {'id': real_snap, 'volume_size': snapshot['volume_size']}) elif source_cg: data = {'consistency-group-id': source_cg['id'], 'snapshot-set-name': group['id']} self.client.req('snapshots', 'POST', data, ver='v2') snap_by_anc = self._get_snapset_ancestors(group['id']) for volume, src_vol in zip(volumes, source_vols): snap_vol_name = snap_by_anc[src_vol['id']] self.client.req('volumes', 'PUT', {'name': volume['id']}, name=snap_vol_name) create_data = {'consistency-group-name': group['id'], 'vol-list': [v['id'] for v in volumes]} self.client.req('consistency-groups', 'POST', data=create_data, ver='v2') return None, None def update_consistencygroup(self, context, group, add_volumes=None, remove_volumes=None): """Updates a consistency group. :param context: the context of the caller. :param group: the dictionary of the consistency group to be updated. :param add_volumes: a list of volume dictionaries to be added. :param remove_volumes: a list of volume dictionaries to be removed. :returns: model_update, add_volumes_update, remove_volumes_update """ add_volumes = add_volumes if add_volumes else [] remove_volumes = remove_volumes if remove_volumes else [] for vol in add_volumes: add_data = {'vol-id': vol['id'], 'cg-id': group['id']} self.client.req('consistency-group-volumes', 'POST', add_data, ver='v2') for vol in remove_volumes: remove_data = {'vol-id': vol['id'], 'cg-id': group['id']} self.client.req('consistency-group-volumes', 'DELETE', remove_data, name=group['id'], ver='v2') return None, None, None def _get_cgsnap_name(self, cgsnapshot): group_id = cgsnapshot.get('group_id') if group_id is None: group_id = cgsnapshot.get('consistencygroup_id') return '%(cg)s%(snap)s' % {'cg': group_id .replace('-', ''), 'snap': cgsnapshot['id'].replace('-', '')} def create_cgsnapshot(self, context, cgsnapshot, snapshots): """Creates a cgsnapshot.""" group_id = cgsnapshot.get('group_id') if group_id is None: group_id = cgsnapshot.get('consistencygroup_id') data = {'consistency-group-id': group_id, 'snapshot-set-name': self._get_cgsnap_name(cgsnapshot)} self.client.req('snapshots', 'POST', data, ver='v2') return None, None def delete_cgsnapshot(self, context, cgsnapshot, snapshots): """Deletes a cgsnapshot.""" self.client.req('snapshot-sets', 'DELETE', name=self._get_cgsnap_name(cgsnapshot), ver='v2') return None, None def create_group(self, context, group): """Creates a group. :param context: the context of the caller. :param group: the group object. :returns: model_update """ # the driver treats a group as a CG internally. # We proxy the calls to the CG api. return self.create_consistencygroup(context, group) def delete_group(self, context, group, volumes): """Deletes a group. :param context: the context of the caller. :param group: the group object. :param volumes: a list of volume objects in the group. :returns: model_update, volumes_model_update """ # the driver treats a group as a CG internally. # We proxy the calls to the CG api. return self.delete_consistencygroup(context, group, volumes) def update_group(self, context, group, add_volumes=None, remove_volumes=None): """Updates a group. :param context: the context of the caller. :param group: the group object. :param add_volumes: a list of volume objects to be added. :param remove_volumes: a list of volume objects to be removed. :returns: model_update, add_volumes_update, remove_volumes_update """ # the driver treats a group as a CG internally. # We proxy the calls to the CG api. return self.update_consistencygroup(context, group, add_volumes, remove_volumes) def create_group_from_src(self, context, group, volumes, group_snapshot=None, snapshots=None, source_group=None, source_vols=None): """Creates a group from source. :param context: the context of the caller. :param group: the Group object to be created. :param volumes: a list of Volume objects in the group. :param group_snapshot: the GroupSnapshot object as source. :param snapshots: a list of snapshot objects in group_snapshot. :param source_group: the Group object as source. :param source_vols: a list of volume objects in the source_group. :returns: model_update, volumes_model_update """ # the driver treats a group as a CG internally. # We proxy the calls to the CG api. return self.create_consistencygroup_from_src(context, group, volumes, group_snapshot, snapshots, source_group, source_vols) def create_group_snapshot(self, context, group_snapshot, snapshots): """Creates a group_snapshot. :param context: the context of the caller. :param group_snapshot: the GroupSnapshot object to be created. :param snapshots: a list of Snapshot objects in the group_snapshot. :returns: model_update, snapshots_model_update """ # the driver treats a group as a CG internally. # We proxy the calls to the CG api. return self.create_cgsnapshot(context, group_snapshot, snapshots) def delete_group_snapshot(self, context, group_snapshot, snapshots): """Deletes a group_snapshot. :param context: the context of the caller. :param group_snapshot: the GroupSnapshot object to be deleted. :param snapshots: a list of snapshot objects in the group_snapshot. :returns: model_update, snapshots_model_update """ # the driver treats a group as a CG internally. # We proxy the calls to the CG api. return self.delete_cgsnapshot(context, group_snapshot, snapshots) def _get_ig(self, name): try: return self.client.req('initiator-groups', 'GET', name=name)['content'] except exception.NotFound: pass def _create_ig(self, name): # create an initiator group to hold the initiator data = {'ig-name': name} self.client.req('initiator-groups', 'POST', data) try: return self.client.req('initiator-groups', name=name)['content'] except exception.NotFound: raise (exception.VolumeBackendAPIException (data=_("Failed to create IG, %s") % name)) @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. ':, ' ``provider_auth`` if present, contains a space-separated triple: ' '. `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) def initialize_connection(self, volume, connector): wwpns = self._get_initiator_names(connector) ig_name = self._get_ig_name(connector) i_t_map = {} found = [] new = [] for wwpn in wwpns: init = self.client.get_initiator(wwpn) if init: found.append(init) else: new.append(wwpn) i_t_map[wwpn.replace(':', '')] = self.get_targets() # get or create initiator group if new: ig = self._get_ig(ig_name) if not ig: ig = self._create_ig(ig_name) for wwpn in new: data = {'initiator-name': wwpn, 'ig-id': ig_name, 'port-address': wwpn} self.client.req('initiators', 'POST', data) igs = list(set([i['ig-id'][XTREMIO_OID_NAME] for i in found])) if new and ig['ig-id'][XTREMIO_OID_NAME] not in igs: igs.append(ig['ig-id'][XTREMIO_OID_NAME]) if len(igs) > 1: lun_num = self._get_free_lun(igs) else: lun_num = None for ig in igs: lunmap = self.create_lun_map(volume, ig, lun_num) lun_num = lunmap['lun'] conn_info = {'driver_volume_type': 'fibre_channel', 'data': { 'target_discovered': False, 'target_lun': lun_num, 'target_wwn': self.get_targets(), 'initiator_target_map': i_t_map}} fczm_utils.add_fc_zone(conn_info) return conn_info def terminate_connection(self, volume, connector, **kwargs): (super(XtremIOFCDriver, self) .terminate_connection(volume, connector, **kwargs)) has_volumes = (not connector or self.client. num_of_mapped_volumes(self._get_ig_name(connector)) > 0) if has_volumes: data = {} else: i_t_map = {} for initiator in self._get_initiator_names(connector): i_t_map[initiator.replace(':', '')] = self.get_targets() data = {'target_wwn': self.get_targets(), 'initiator_target_map': i_t_map} conn_info = {'driver_volume_type': 'fibre_channel', 'data': data} fczm_utils.remove_fc_zone(conn_info) return conn_info def _get_initiator_names(self, connector): return [wwpn if ':' in wwpn else ':'.join(wwpn[i:i + 2] for i in range(0, len(wwpn), 2)) for wwpn in connector['wwpns']] def _get_ig_name(self, connector): return connector['host']