From 8eb05e45d879b072ea25ac645586c7e2f832f88c Mon Sep 17 00:00:00 2001 From: Shay Halsband Date: Sun, 8 Mar 2015 14:34:53 +0200 Subject: [PATCH] XtremIO Volume driver requests, multipath * introduce new driver parameter to configure ssl CA validation * replace urllib2 with requests * changes to support iSCSI multipath * fix missing logging for get requets * query cluster version without exception DocImpact: added the option driver_ssl_cert_verify to specify whether a volume backends verify SSL certificats when quering https urls. Implements: blueprint emc-xtremio-updates Change-Id: Ief32e2247a46046489792f03a5b235d1c9d16a4f --- cinder/tests/unit/test_emc_xtremio.py | 42 ++++- cinder/volume/driver.py | 4 + cinder/volume/drivers/emc/xtremio.py | 230 +++++++++++++------------- 3 files changed, 155 insertions(+), 121 deletions(-) diff --git a/cinder/tests/unit/test_emc_xtremio.py b/cinder/tests/unit/test_emc_xtremio.py index 9b28a6b67..c0e4b50a0 100644 --- a/cinder/tests/unit/test_emc_xtremio.py +++ b/cinder/tests/unit/test_emc_xtremio.py @@ -32,7 +32,17 @@ typ2id = {'volumes': 'vol-id', 'lun-maps': 'mapping-id'} xms_data = {'xms': {1: {'version': '4.0.0'}}, - 'clusters': {1: {'sys-sw-version': "3.0.0-devel_ba23ee5381eeab73", + 'clusters': {'cluster1': + {'name': 'cluster1', + 'sys-sw-version': "3.0.0-devel_ba23ee5381eeab73", + 'ud-ssd-space': '8146708710', + 'ud-ssd-space-in-use': '708710', + 'vol-size': '29884416', + 'chap-authentication-mode': 'disabled', + 'chap-discovery-mode': 'disabled', + "index": 1}, + 1: {'name': 'cluster1', + 'sys-sw-version': "3.0.0-devel_ba23ee5381eeab73", 'ud-ssd-space': '8146708710', 'ud-ssd-space-in-use': '708710', 'vol-size': '29884416', @@ -109,10 +119,13 @@ def xms_request(object_type='volumes', request_typ='GET', data=None, raise exception.NotFound() return {"content": res[obj_key]} else: - return {object_type: [{"href": "/%s/%d" % (object_type, - obj['index']), - "name": obj.get('name')} - for obj in res.values()]} + if data and data.get('full') == 1: + return {object_type: res.values()} + else: + return {object_type: [{"href": "/%s/%d" % (object_type, + obj['index']), + "name": obj.get('name')} + for obj in res.values()]} elif request_typ == 'POST': data = fix_data(data, object_type) data['index'] = len(xms_data[object_type]) + 1 @@ -235,10 +248,11 @@ class EMCXIODriverISCSITestCase(test.TestCase): def test_check_for_setup_error(self, req): req.side_effect = xms_request - xms = xms_data['xms'] - del xms_data['xms'] - self.driver.check_for_setup_error() - xms_data['xms'] = xms + clusters = xms_data['clusters'] + del xms_data['clusters'] + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + xms_data['clusters'] = clusters self.driver.check_for_setup_error() def test_create_extend_delete_volume(self, req): @@ -285,6 +299,16 @@ class EMCXIODriverISCSITestCase(test.TestCase): self.driver.create_volume, self.data.test_volume) self.driver.delete_volume(self.data.test_volume) + def test_no_portals_configured(self, req): + req.side_effect = xms_request + clean_xms_data() + portals = xms_data['iscsi-portals'].copy() + xms_data['iscsi-portals'].clear() + lunmap = {'lun': 4} + self.assertRaises(exception.VolumeDriverException, + self.driver._get_iscsi_properties, lunmap) + xms_data['iscsi-portals'] = portals + def test_initialize_terminate_connection(self, req): req.side_effect = xms_request clean_xms_data() diff --git a/cinder/volume/driver.py b/cinder/volume/driver.py index 970449695..b9a06aecd 100644 --- a/cinder/volume/driver.py +++ b/cinder/volume/driver.py @@ -211,6 +211,10 @@ volume_opts = [ 'used to determine the goodness of a host. Only used ' 'when using the goodness weigher is set to be used by ' 'the Cinder scheduler.'), + cfg.BoolOpt('driver_ssl_cert_verify', + default=False, + help='If set to True the http client will validate the SSL ' + 'certificate of the backend endpoint.'), ] # for backward compatibility diff --git a/cinder/volume/drivers/emc/xtremio.py b/cinder/volume/drivers/emc/xtremio.py index 4a7d7be55..d4ec195e5 100644 --- a/cinder/volume/drivers/emc/xtremio.py +++ b/cinder/volume/drivers/emc/xtremio.py @@ -22,19 +22,19 @@ supported XtremIO version 2.4 and up 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 and CA validation """ -import base64 import json import math import random +import requests import string from oslo_config import cfg from oslo_log import log as logging from oslo_utils import units import six -from six.moves import urllib from cinder import exception from cinder.i18n import _, _LE, _LI, _LW @@ -65,71 +65,18 @@ class XtremIOClient(object): def __init__(self, configuration, cluster_id): self.configuration = configuration self.cluster_id = cluster_id - 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) - - def _create_request(self, request_typ, data, url, url_data): - if request_typ in ('GET', 'DELETE'): - data.update(url_data) - self.update_url(data, self.cluster_id) - query = urllib.parse.urlencode(data, doseq=True) - url = '%(url)s?%(query)s' % {'query': query, 'url': url} - request = urllib.request.Request(url) - else: - if url_data: - url = ('%(url)s?%(query)s' % - {'query': urllib.parse.urlencode(url_data, doseq=True), - 'url': url}) - - self.update_data(data, self.cluster_id) - LOG.debug('data: %s', data) - request = urllib.request.Request(url, json.dumps(data)) - LOG.debug('%(type)s %(url)s', {'type': request_typ, 'url': url}) + self.verify = (self.configuration. + safe_get('driver_ssl_cert_verify') + or False) - def get_request_type(): - return request_typ - request.get_method = get_request_type - request.add_header("Authorization", "Basic %s" % (self.base64_auth, )) - return request - - def _send_request(self, object_type, key, request): - try: - response = urllib.request.urlopen(request) - except (urllib.error.HTTPError, ) as exc: - if exc.code == 400 and hasattr(exc, 'read'): - error = json.load(exc) - err_msg = error['message'] - if err_msg.endswith(OBJ_NOT_FOUND_ERR): - LOG.warning(_LW("object %(key)s of " - "type %(typ)s not found"), - {'key': key, 'typ': object_type}) - raise exception.NotFound() - elif err_msg == VOL_NOT_UNIQUE_ERR: - LOG.error(_LE("can't create 2 volumes with the same name")) - msg = (_('Volume by this name already exists')) - raise exception.VolumeBackendAPIException(data=msg) - elif err_msg == VOL_OBJ_NOT_FOUND_ERR: - LOG.error(_LE("Can't find volume to map %s"), key) - raise exception.VolumeNotFound(volume_id=key) - elif ALREADY_MAPPED_ERR in err_msg: - raise exception.XtremIOAlreadyMappedError() - 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 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', request_typ='GET', data=None, - name=None, idx=None): + name=None, idx=None, ver='v1'): if not data: data = {} if name and idx: @@ -137,27 +84,64 @@ class XtremIOClient(object): LOG.error(msg) raise exception.VolumeDriverException(message=msg) - url = '%s/%s' % (self.base_url, object_type) - url_data = {} + url = '%s/%s' % (self.get_base_url(ver), object_type) + params = {} key = None if name: - url_data['name'] = name + params['name'] = name key = name elif idx: url = '%s/%d' % (url, idx) key = str(idx) - request = self._create_request(request_typ, data, url, url_data) - 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}) + if request_typ in ('GET', 'DELETE'): + params.update(data) + self.update_url(params, self.cluster_id) + if request_typ != 'GET': + self.update_data(data, self.cluster_id) + LOG.debug('data: %s', data) + LOG.debug('%(type)s %(url)s', {'type': request_typ, 'url': url}) + try: + response = requests.request(request_typ, 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 200 <= response.status_code < 300: + if request_typ in ('GET', 'POST'): + return response.json() + else: + return '' + + self.handle_errors(response, key, object_type) + + def handle_errors(self, response, key, object_type): + if response.status_code == 400: + error = response.json() + err_msg = error.get('message') + if err_msg.endswith(OBJ_NOT_FOUND_ERR): + LOG.warning(_LW("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(_LE("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(_LE("Can't find volume to map %(key)s, %(msg)s"), + {'key': key, 'msg': err_msg, }) + raise exception.VolumeNotFound(volume_id=key) + elif ALREADY_MAPPED_ERR in err_msg: + raise exception.XtremIOAlreadyMappedError() + msg = _('Bad response from XMS, %s') % response.text + LOG.error(msg) + raise exception.VolumeBackendAPIException(message=msg) def update_url(self, data, cluster_id): return @@ -170,6 +154,10 @@ class XtremIOClient(object): 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: for lm_link in self.req('lun-maps')['lun-maps']: @@ -191,22 +179,28 @@ class XtremIOClient3(XtremIOClient): cnt += 1 return cnt - def get_iscsi_portal(self): + def get_iscsi_portals(self): + if self._portals: + return self._portals + iscsi_portals = [t['name'] for t in self.req('iscsi-portals') ['iscsi-portals']] - # Get a random portal - portal_name = 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)) + 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 portal + return self._portals class XtremIOClient4(XtremIOClient): + def __init__(self, configuration, cluster_id): + super(XtremIOClient4, self).__init__(configuration, cluster_id) + self._cluster_name = None + def find_lunmap(self, ig_name, vol_name): try: return (self.req('lun-maps', @@ -230,23 +224,21 @@ class XtremIOClient4(XtremIOClient): if cluster_id: data['cluster-id'] = cluster_id - def get_iscsi_portal(self): - iscsi_portals = self.req('iscsi-portals', - data={'full': 1})['iscsi-portals'] - return RANDOM.choice(iscsi_portals) + def get_iscsi_portals(self): + return self.req('iscsi-portals', + data={'full': 1})['iscsi-portals'] def get_cluster(self): - if self.cluster_id: - return self.req('clusters', name=self.cluster_id)['content'] - else: - name = self.req('clusters')['clusters'][0]['name'] - return self.req('clusters', name=name)['content'] + if not self.cluster_id: + self.cluster_id = self.req('clusters')['clusters'][0]['name'] + + return self.req('clusters', name=self.cluster_id)['content'] class XtremIOVolumeDriver(san.SanDriver): """Executes commands relating to Volumes.""" - VERSION = '1.0.5' + VERSION = '1.0.6' driver_name = 'XtremIO' MIN_XMS_VERSION = [3, 0, 0] @@ -270,12 +262,9 @@ class XtremIOVolumeDriver(san.SanDriver): def check_for_setup_error(self): try: - try: - xms = self.client.req('xms', idx=1)['content'] - version_text = xms['version'] - except exception.VolumeDriverException: - cluster = self.client.req('clusters', idx=1)['content'] - version_text = cluster['sys-sw-version'] + 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 @@ -435,8 +424,7 @@ class XtremIOVolumeDriver(san.SanDriver): vol = self.client.req('volumes', name=volume['id'])['content'] lm_name = '%s_%s_%s' % (six.text_type(vol['index']), - six.text_type(ig['index']) - if ig else 'any', + six.text_type(ig['index']), six.text_type(tg['index'])) LOG.debug('removing lun map %s', lm_name) self.client.req('lun-maps', 'DELETE', name=lm_name) @@ -503,7 +491,7 @@ class XtremIOISCSIDriver(XtremIOVolumeDriver, driver.ISCSIDriver): ig = self.client.req('initiator-groups', 'GET', name=self._get_ig(connector))['content'] except exception.NotFound: - # create an initiator group to hold the the initiator + # create an initiator group to hold the initiator data = {'ig-name': self._get_ig(connector)} self.client.req('initiator-groups', 'POST', data) try: @@ -569,14 +557,32 @@ class XtremIOISCSIDriver(XtremIOVolumeDriver, driver.ISCSIDriver): meaning use CHAP with the specified credentials. :access_mode: the volume access mode allow client used ('rw' or 'ro' currently supported) + 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. """ - portal = self.client.get_iscsi_portal() - ip = portal['ip-addr'].split('/')[0] + 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': '%s:%d' % (ip, portal['ip-port']), - 'access_mode': 'rw'} + 'target_portal': portal_addr, + 'access_mode': 'rw', + 'target_iqns': [p['port-address'] for p in portals], + 'target_portals': tg_portals, + 'target_luns': [lunmap['lun']] * len(portals)} return properties def _get_initiator(self, connector): -- 2.45.2