# Copyright (c) 2012 - 2014 EMC Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Driver for EMC XtremIO Storage. supported XtremIO version 2.4 and up 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 """ import base64 import json import random import string import urllib import urllib2 import six from cinder import exception from cinder.i18n import _, _LE, _LI, _LW from cinder.openstack.common import log as logging from cinder.volume import driver from cinder.volume.drivers.san import san from cinder.zonemanager import utils as fczm_utils LOG = logging.getLogger(__name__) class XtremIOVolumeDriver(san.SanDriver): """Executes commands relating to Volumes.""" VERSION = '1.0.4' driver_name = 'XtremIO' MIN_XMS_VERSION = [2, 4, 0] random = random.Random() def __init__(self, *args, **kwargs): super(XtremIOVolumeDriver, self).__init__(*args, **kwargs) self.base64_auth = (base64.encodestring('%s:%s' % (self.configuration.san_login, self.configuration.san_password)) .replace('\n', '')) self.base_url = ('https://%s/api/json/types' % self.configuration.san_ip) self.protocol = None self.backend_name = (self.configuration.safe_get( 'volume_backend_name') or self.driver_name) def _create_request(self, request_typ, data, url, urllib): if data and request_typ == 'GET': url + '?' + urllib.urlencode(data) request = urllib2.Request(url) elif data: LOG.debug('data: %s', json.dumps(data)) request = urllib2.Request(url, json.dumps(data)) else: request = urllib2.Request(url) LOG.debug('quering url: %s', url) request.get_method = lambda: request_typ request.add_header("Authorization", "Basic %s" % (self.base64_auth, )) return request def _send_request(self, object_type, key, request): try: response = urllib2.urlopen(request) except (urllib2.HTTPError, ) as exc: if exc.code == 400 and hasattr(exc, 'read'): error = json.load(exc) if error['message'].endswith('obj_not_found'): LOG.warning(_LW("object %(key)s of " "type %(typ)s not found"), {'key': key, 'typ': object_type}) raise exception.NotFound() elif error['message'] == 'vol_obj_name_not_unique': LOG.error(_LE("can't create 2 volumes with the same name")) msg = (_('Volume by this name already exists')) raise exception.VolumeBackendAPIException(data=msg) LOG.error(_LE('Bad response from XMS, %s'), exc.read()) msg = (_('Exception: %s') % six.text_type(exc)) raise exception.VolumeDriverException(message=msg) if response.code >= 300: LOG.error(_LE('bad API response, %s'), response.msg) msg = (_('bad response from XMS got http code %(code)d, %(msg)s') % {'code': response.code, 'msg': response.msg}) raise exception.VolumeBackendAPIException(data=msg) return response def req(self, object_type='volumes', request_typ='GET', data=None, name=None, idx=None): 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.base_url, object_type) key = None if name: url = '%s?%s' % (url, urllib.urlencode({'name': name})) key = name elif idx: url = '%s/%d' % (url, idx) key = str(idx) request = self._create_request(request_typ, data, url, urllib) response = self._send_request(object_type, key, request) str_result = response.read() if str_result: try: return json.loads(str_result) except Exception: LOG.exception(_LE('querying %(typ)s, %(req)s failed to ' 'parse result, return value = %(res)s'), {'typ': object_type, 'req': request_typ, 'res': str_result}) def _obj_from_result(self, res): typ, idx = res['links'][0]['href'].split('/')[-2:] return self.req(typ, idx=int(idx))['content'] def check_for_setup_error(self): try: sys = self.req('clusters', idx=1)['content'] except exception.NotFound: msg = _("XtremIO not initialized correctly, no clusters found") raise (exception.VolumeBackendAPIException (data=msg)) ver = [int(n) for n in sys['sys-sw-version'].split('-')[0].split('.')] if ver < self.MIN_XMS_VERSION: msg = _('Invalid XtremIO version %s,' ' version 2.4 or up is required') % sys['sys-sw-version'] LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) else: LOG.info(_LI('XtremIO SW version %s'), sys['sys-sw-version']) def create_volume(self, volume): "Creates a volume" data = {'vol-name': volume['id'], 'vol-size': str(volume['size']) + 'g' } self.req('volumes', 'POST', data) def create_volume_from_snapshot(self, volume, snapshot): """Creates a volume from a snapshot.""" data = {'snap-vol-name': volume['id'], 'ancestor-vol-id': snapshot.id} self.req('snapshots', 'POST', data) def create_cloned_volume(self, volume, src_vref): """Creates a clone of the specified volume.""" data = {'snap-vol-name': volume['id'], 'ancestor-vol-id': src_vref['id']} self.req('snapshots', 'POST', data) def delete_volume(self, volume): """Deletes a volume.""" try: self.req('volumes', 'DELETE', name=volume['id']) except exception.NotFound: LOG.info(_LI("volume %s doesn't exist"), volume['id']) def create_snapshot(self, snapshot): """Creates a snapshot.""" data = {'snap-vol-name': snapshot.id, 'ancestor-vol-id': snapshot.volume_id} self.req('snapshots', 'POST', data) def delete_snapshot(self, snapshot): """Deletes a snapshot.""" try: self.req('volumes', 'DELETE', name=snapshot.id) except exception.NotFound: LOG.info(_LI("snapshot %s doesn't exist"), snapshot.id) def _update_volume_stats(self): self._stats = {'volume_backend_name': self.backend_name, 'vendor_name': 'EMC', 'driver_version': self.VERSION, 'storage_protocol': self.protocol, 'total_capacity_gb': 'infinite', 'free_capacity_gb': 'infinite', 'reserved_percentage': 0, 'QoS_support': False} def get_volume_stats(self, refresh=False): """Get volume stats. If 'refresh' is True, run update the stats first. """ if refresh: self._update_volume_stats() return self._stats def extend_volume(self, volume, new_size): """Extend an existing volume's size.""" data = {'vol-size': str(new_size) + 'g'} try: self.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""" try: ig = self.req('initiator-groups', name=self._get_ig(connector))['content'] tg = self.req('target-groups', name='Default')['content'] vol = self.req('volumes', name=volume['id'])['content'] lm_name = '%s_%s_%s' % (str(vol['index']), str(ig['index']) if ig else 'any', str(tg['index'])) LOG.info(_LI('removing lun map %s'), lm_name) self.req('lun-maps', 'DELETE', name=lm_name) except exception.NotFound: LOG.warning(_LW("terminate_connection: lun map not found")) def _find_lunmap(self, ig_name, vol_name): try: for lm_link in self.req('lun-maps')['lun-maps']: idx = lm_link['href'].split('/')[-1] lm = self.req('lun-maps', idx=int(idx))['content'] if lm['ig-name'] == ig_name and lm['vol-name'] == vol_name: return lm except exception.NotFound: raise (exception.VolumeDriverException (_("can't find lunmap, ig:%(ig)s vol:%(vol)s") % {'ig': ig_name, 'vol': vol_name})) 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] lm = self.req('lun-maps', idx=int(idx))['content'] if lm['ig-name'] == initiator: cnt += 1 return cnt def _get_password(self): return ''.join(self.random.choice (string.ascii_uppercase + string.digits) for _ in range(12)) def create_lun_map(self, volume, ig): try: res = self.req('lun-maps', 'POST', {'ig-id': ig['ig-id'][2], 'vol-id': volume['id']}) lunmap = self._obj_from_result(res) LOG.info(_LI('created lunmap\n%s'), lunmap) except urllib2.HTTPError as exc: if exc.code == 400: error = json.load(exc) if 'already_mapped' in error.message: LOG.info(_LI('volume already mapped,' ' trying to retrieve it %(ig)s, %(vol)d'), {'ig': ig['ig-id'][1], 'vol': volume['id']}) lunmap = self._find_lunmap(ig['ig-id'][1], volume['id']) elif error.message == 'vol_obj_not_found': LOG.error(_LE("Can't find volume to map %s"), volume['id']) raise exception.VolumeNotFound(volume_id=volume['id']) else: raise else: raise return lunmap def _get_ig(self, connector): raise NotImplementedError() 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 initialize_connection(self, volume, connector): # FIXME(shay-halsband): query the cluster index instead of using # the 1st one try: sys = self.req('clusters', idx=1)['content'] except exception.NotFound: msg = _("XtremIO not initialized correctly, no clusters found") raise exception.VolumeBackendAPIException(data=msg) use_chap = (sys.get('chap-authentication-mode', 'disabled') != 'disabled') discovery_chap = (sys.get('chap-discovery-mode', 'disabled') != 'disabled') initiator = self._get_initiator(connector) try: # check if the IG already exists ig = self.req('initiator-groups', 'GET', name=self._get_ig(connector))['content'] except exception.NotFound: # create an initiator group to hold the the initiator data = {'ig-name': self._get_ig(connector)} self.req('initiator-groups', 'POST', data) try: ig = self.req('initiator-groups', name=self._get_ig(connector))['content'] except exception.NotFound: raise (exception.VolumeBackendAPIException (data=_("Failed to create IG, %s") % self._get_ig(connector))) try: init = self.req('initiators', 'GET', name=initiator)['content'] if use_chap: chap_passwd = init['chap-authentication-initiator-' 'password'] # delete the initiator to create a new one with password if not chap_passwd: LOG.info(_LI('initiator has no password while using chap,' 'removing it')) self.req('initiators', 'DELETE', name=initiator) # check if the initiator already exists raise exception.NotFound() except exception.NotFound: # create an initiator data = {'initiator-name': initiator, 'ig-id': initiator, 'port-address': initiator} if use_chap: data['initiator-authentication-user-name'] = 'chap_user' chap_passwd = self._get_password() data['initiator-authentication-password'] = chap_passwd if discovery_chap: data['initiator-discovery-user-name'] = 'chap_user' data['initiator-discovery-' 'password'] = self._get_password() self.req('initiators', 'POST', data) # lun mappping lunmap = self.create_lun_map(volume, ig) properties = self._get_iscsi_properties(lunmap) if use_chap: properties['auth_method'] = 'CHAP' properties['auth_username'] = 'chap_user' properties['auth_password'] = chap_passwd LOG.debug('init conn params:\n%s', 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. :access_mode: the volume access mode allow client used ('rw' or 'ro' currently supported) """ iscsi_portals = [t['name'] for t in self.req('iscsi-portals') ['iscsi-portals']] # get a random portal portal_name = self.random.choice(iscsi_portals) try: portal = self.req('iscsi-portals', name=portal_name)['content'] except exception.NotFound: raise (exception.VolumeBackendAPIException (data=_("iscsi portal, %s, not found") % portal_name)) ip = portal['ip-addr'].split('/')[0] properties = {'target_discovered': False, 'target_iqn': portal['port-address'], 'target_lun': lunmap['lun'], 'target_portal': '%s:%d' % (ip, portal['ip-port']), 'access_mode': 'rw'} return properties def _get_initiator(self, connector): return connector['initiator'] def _get_ig(self, connector): return connector['initiator'] class XtremIOFibreChannelDriver(XtremIOVolumeDriver, driver.FibreChannelDriver): def __init__(self, *args, **kwargs): super(XtremIOFibreChannelDriver, self).__init__(*args, **kwargs) self.protocol = 'FC' def get_targets(self): if not hasattr(self, '_targets'): try: target_list = self.req('targets')["targets"] targets = [self.req('targets', name=target['name'])['content'] for target in target_list if '-fc' in target['name']] self._targets = [target['port-address'].replace(':', '') for target in targets if target['port-state'] == 'up'] except exception.NotFound: raise (exception.VolumeBackendAPIException (data=_("Failed to get targets"))) return self._targets @fczm_utils.AddFCZone def initialize_connection(self, volume, connector): initiators = self._get_initiator(connector) ig_name = self._get_ig(connector) i_t_map = {} # get or create initiator group try: # check if the IG already exists ig = self.req('initiator-groups', name=ig_name)['content'] except exception.NotFound: # create an initiator group to hold the the initiator data = {'ig-name': ig_name} self.req('initiator-groups', 'POST', data) try: ig = self.req('initiator-groups', name=ig_name)['content'] except exception.NotFound: raise (exception.VolumeBackendAPIException (data=_("Failed to create IG, %s") % ig_name)) # get or create all initiators for initiator in initiators: try: self.req('initiators', name=initiator)['content'] except exception.NotFound: # create an initiator data = {'initiator-name': initiator, 'ig-id': ig['name'], 'port-address': initiator} self.req('initiators', 'POST', data) i_t_map[initiator] = self.get_targets() lunmap = self.create_lun_map(volume, ig) return {'driver_volume_type': 'fibre_channel', 'data': { 'target_discovered': True, 'target_lun': lunmap['lun'], 'target_wwn': self.get_targets(), 'access_mode': 'rw', 'initiator_target_map': i_t_map}} @fczm_utils.RemoveFCZone def terminate_connection(self, volume, connector, **kwargs): (super(XtremIOFibreChannelDriver, self) .terminate_connection(volume, connector, **kwargs)) num_vols = self._num_of_mapped_volumes(self._get_ig(connector)) if num_vols > 0: data = {} else: i_t_map = {} for initiator in self._get_initiator(connector): i_t_map[initiator] = self.get_targets() data = {'target_wwn': self.get_targets(), 'initiator_target_map': i_t_map} return {'driver_volume_type': 'fibre_channel', 'data': data} def _get_initiator(self, connector): return connector['wwpns'] def _get_ig(self, connector): return connector['host']