]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
XtremIO Volume driver requests, multipath
authorShay Halsband <shay.halsband@emc.com>
Sun, 8 Mar 2015 12:34:53 +0000 (14:34 +0200)
committerShay Halsband <shay.halsband@emc.com>
Wed, 17 Jun 2015 11:04:05 +0000 (14:04 +0300)
* 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
cinder/volume/driver.py
cinder/volume/drivers/emc/xtremio.py

index 9b28a6b677fa4a390bca644593a4222985cbcc8f..c0e4b50a02fea6243ff2104bf6a18947a1dd2f05 100644 (file)
@@ -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()
index 9704496956430ec7ec3ce9ea2e9a7f35bbd7819e..b9a06aecdff496ac9fe4dcca549e3c5485f1d611 100644 (file)
@@ -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
index 4a7d7be5596d6641d47792b19963813a3f25227c..d4ec195e5ebcf85795e39f48be2a1a1978ed503f 100644 (file)
@@ -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):