]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
XtremIO support for iscsi discovery auth
authorShay Halsband <shay.halsband@emc.com>
Mon, 8 Jun 2015 16:08:46 +0000 (19:08 +0300)
committerShay Halsband <shay.halsband@emc.com>
Sun, 26 Jul 2015 14:44:25 +0000 (17:44 +0300)
- Discover existing Initiator Groups (a grouping object for initiators)
  from connection information.
- add unit tests for better code coverage.
- handle 'system is busy' array error.

Change-Id: Ia5df488ed0996fb6fb1b77016772e82323ac1f92
Partial-Implements: blueprint emc-xtremio-updates

cinder/exception.py
cinder/tests/unit/test_emc_xtremio.py
cinder/volume/drivers/emc/xtremio.py

index aef6c127eaf4cbe1948368587c264a2ca29bc866..2e312d8c4e59e0a24cae04bf86a299f9981d07ba 100644 (file)
@@ -932,6 +932,10 @@ class XtremIOAlreadyMappedError(CinderException):
     message = _("Volume to Initiator Group mapping already exists")
 
 
+class XtremIOArrayBusy(CinderException):
+    message = _("System is busy, retry operation.")
+
+
 # StorPool driver
 class StorPoolConfigurationMissing(CinderException):
     message = _("Missing parameter %(param)s in the %(section)s section "
index 13f0b23a6229441a853d3b353e70dc214d36f842..c8ca07e07f241520795e23210c46ee218d5ff25e 100644 (file)
@@ -377,9 +377,38 @@ class EMCXIODriverISCSITestCase(test.TestCase):
         map_data = self.driver.initialize_connection(self.data.test_volume,
                                                      self.data.connector)
         self.assertEqual(map_data['data']['target_lun'], 1)
+        i1 = xms_data['initiators'][1]
+        i1['ig-id'] = ['', i1['ig-id'], 1]
+        i1['chap-authentication-initiator-password'] = 'chap_password1'
+        i1['chap-discovery-initiator-password'] = 'chap_password2'
+        map_data = self.driver.initialize_connection(self.data.test_volume2,
+                                                     self.data.connector)
         self.driver.terminate_connection(self.data.test_volume,
                                          self.data.connector)
 
+    def test_initialize_chap_connection(self, req):
+        req.side_effect = xms_request
+        clean_xms_data()
+        self.driver.create_volume(self.data.test_volume)
+        map_data = self.driver.initialize_connection(self.data.test_volume,
+                                                     self.data.connector)
+        c1 = xms_data['clusters'][1]
+        c1['chap-authentication-mode'] = 'initiator'
+        c1['chap-discovery-mode'] = 'initiator'
+        i1 = xms_data['initiators'][1]
+        i1['ig-id'] = ['', i1['ig-id'], 1]
+        i1['chap-authentication-initiator-password'] = 'chap_password1'
+        i1['chap-discovery-initiator-password'] = 'chap_password2'
+        map_data = self.driver.initialize_connection(self.data.test_volume2,
+                                                     self.data.connector)
+        self.assertEqual('chap_password1', map_data['data']['auth_password'])
+        self.assertEqual('chap_password2',
+                         map_data['data']['discovery_auth_password'])
+        i1['chap-authentication-initiator-password'] = None
+        i1['chap-discovery-initiator-password'] = None
+        map_data = self.driver.initialize_connection(self.data.test_volume2,
+                                                     self.data.connector)
+
     def test_initialize_connection_bad_ig(self, req):
         req.side_effect = xms_bad_request
         self.assertRaises(exception.VolumeBackendAPIException,
@@ -447,6 +476,47 @@ class EMCXIODriverISCSITestCase(test.TestCase):
         self.driver.delete_consistencygroup(d.context, d.group)
 
 
+@mock.patch('requests.request')
+class EMCXIODriverTestCase(test.TestCase):
+    def setUp(self):
+        super(EMCXIODriverTestCase, self).setUp()
+
+        configuration = mock.Mock()
+        configuration.san_login = ''
+        configuration.san_password = ''
+        configuration.san_ip = ''
+        configuration.xtremio_cluster_name = ''
+
+        def safe_get(key):
+            getattr(configuration, key)
+
+        configuration.safe_get = safe_get
+        self.driver = xtremio.XtremIOISCSIDriver(configuration=configuration)
+
+        self.data = CommonData()
+
+    def test_retry_request(self, req):
+        busy_response = mock.MagicMock()
+        busy_response.status_code = 400
+        busy_response.json.return_value = {
+            "message": "system_is_busy",
+            "error_code": 400
+        }
+        good_response = mock.MagicMock()
+        good_response.status_code = 200
+
+        EMCXIODriverTestCase.req_count = 0
+
+        def busy_request(*args, **kwargs):
+            if EMCXIODriverTestCase.req_count < 1:
+                EMCXIODriverTestCase.req_count += 1
+                return busy_response
+            return good_response
+
+        req.side_effect = busy_request
+        self.driver.create_volume(self.data.test_volume)
+
+
 @mock.patch('cinder.volume.drivers.emc.xtremio.XtremIOClient.req')
 class EMCXIODriverFibreChannelTestCase(test.TestCase):
     def setUp(self):
index 2e604419e2e0926edca62b07b0798b0dfcbab417..4a6a79b9cd1babaa0733fdded7122f071de1be06 100644 (file)
@@ -23,7 +23,7 @@ supported XtremIO version 2.4 and up
 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
+        R/O snapshots, CHAP discovery authentication
 """
 
 import json
@@ -40,6 +40,7 @@ import six
 from cinder import exception
 from cinder.i18n import _, _LE, _LI, _LW
 from cinder import objects
+from cinder import utils
 from cinder.volume import driver
 from cinder.volume.drivers.san import san
 from cinder.zonemanager import utils as fczm_utils
@@ -52,7 +53,13 @@ DEFAULT_PROVISIONING_FACTOR = 20.0
 XTREMIO_OPTS = [
     cfg.StrOpt('xtremio_cluster_name',
                default='',
-               help='XMS cluster id in multi-cluster environment')]
+               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')]
 
 CONF.register_opts(XTREMIO_OPTS)
 
@@ -61,6 +68,7 @@ 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'
 
 
 class XtremIOClient(object):
@@ -77,6 +85,9 @@ class XtremIOClient(object):
         elif ver == 'v2':
             return 'https://%s/api/json/v2/types' % self.configuration.san_ip
 
+    @utils.retry(exception.XtremIOArrayBusy,
+                 CONF.xtremio_array_busy_retry_count,
+                 CONF.xtremio_array_busy_retry_interval, 1)
     def req(self, object_type='volumes', request_typ='GET', data=None,
             name=None, idx=None, ver='v1'):
         if not data:
@@ -141,6 +152,8 @@ class XtremIOClient(object):
                 raise exception.VolumeNotFound(volume_id=key)
             elif ALREADY_MAPPED_ERR in err_msg:
                 raise exception.XtremIOAlreadyMappedError()
+            elif err_msg == SYSTEM_BUSY:
+                raise exception.XtremIOArrayBusy()
         msg = _('Bad response from XMS, %s') % response.text
         LOG.error(msg)
         raise exception.VolumeBackendAPIException(message=msg)
@@ -168,6 +181,9 @@ class XtremIOClient(object):
     def get_extra_capabilities(self):
         return {}
 
+    def get_initiator(self, port_address):
+        raise NotImplementedError()
+
 
 class XtremIOClient3(XtremIOClient):
     def __init__(self, configuration, cluster_id):
@@ -183,7 +199,7 @@ class XtremIOClient3(XtremIOClient):
                     return lm
         except exception.NotFound:
             raise (exception.VolumeDriverException
-                   (_("can't find lunmap, ig:%(ig)s vol:%(vol)s") %
+                   (_("can't find lun-map, ig:%(ig)s vol:%(vol)s") %
                     {'ig': ig_name, 'vol': vol_name}))
 
     def num_of_mapped_volumes(self, initiator):
@@ -216,6 +232,12 @@ class XtremIOClient3(XtremIOClient):
 
         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):
@@ -281,6 +303,15 @@ class XtremIOClient4(XtremIOClient):
         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
+
 
 class XtremIOVolumeDriver(san.SanDriver):
     """Executes commands relating to Volumes."""
@@ -336,7 +367,7 @@ class XtremIOVolumeDriver(san.SanDriver):
                 }
         self.client.req('volumes', 'POST', data)
 
-        if volume.get('consistencygroup_id'):
+        if volume.get('consistencygroup_id') and self.client is XtremIOClient4:
             self.client.add_vol_to_cg(volume['id'],
                                       volume['consistencygroup_id'])
 
@@ -344,7 +375,8 @@ class XtremIOVolumeDriver(san.SanDriver):
         """Creates a volume from a snapshot."""
         self.client.create_snapshot(snapshot.id, volume['id'])
 
-        if snapshot.get('consistencygroup_id'):
+        if (snapshot.get('consistencygroup_id') and
+                self.client is XtremIOClient4):
             self.client.add_vol_to_cg(volume['id'],
                                       snapshot['consistencygroup_id'])
 
@@ -352,7 +384,7 @@ class XtremIOVolumeDriver(san.SanDriver):
         """Creates a clone of the specified volume."""
         self.client.create_snapshot(src_vref['id'], volume['id'])
 
-        if volume.get('consistencygroup_id'):
+        if volume.get('consistencygroup_id') and self.client is XtremIOClient4:
             self.client.add_vol_to_cg(volume['id'],
                                       volume['consistencygroup_id'])
 
@@ -470,14 +502,14 @@ class XtremIOVolumeDriver(san.SanDriver):
         """Disallow connection from connector"""
         try:
             ig = self.client.req('initiator-groups',
-                                 name=self._get_ig(connector))['content']
+                                 name=self._get_ig_name(connector))['content']
             tg = self.client.req('target-groups', name='Default')['content']
             vol = self.client.req('volumes', name=volume['id'])['content']
 
             lm_name = '%s_%s_%s' % (six.text_type(vol['index']),
                                     six.text_type(ig['index']),
                                     six.text_type(tg['index']))
-            LOG.debug('removing lun map %s', lm_name)
+            LOG.debug('Removing lun map %s.', lm_name)
             self.client.req('lun-maps', 'DELETE', name=lm_name)
         except exception.NotFound:
             LOG.warning(_LW("terminate_connection: lun map not found"))
@@ -487,21 +519,22 @@ class XtremIOVolumeDriver(san.SanDriver):
                        (string.ascii_uppercase + string.digits)
                        for _ in range(12))
 
-    def create_lun_map(self, volume, ig):
+    def create_lun_map(self, volume, ig, lun_num=None):
         try:
-            res = self.client.req('lun-maps', 'POST',
-                                  {'ig-id': ig['ig-id'][2],
-                                   'vol-id': volume['id']})
+            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(_LI('created lunmap\n%s'), lunmap)
+            LOG.info(_LI('Created lun-map:\n%s'), lunmap)
         except exception.XtremIOAlreadyMappedError:
-            LOG.info(_LI('volume already mapped,'
-                         ' trying to retrieve it %(ig)s, %(vol)d'),
-                     {'ig': ig['ig-id'][1], 'vol': volume['id']})
-            lunmap = self.client.find_lunmap(ig['ig-id'][1], volume['id'])
+            LOG.info(_LI('Volume already mapped, retrieving %(ig)s, %(vol)d'),
+                     {'ig': ig, 'vol': volume['id']})
+            lunmap = self.client.find_lunmap(ig, volume['id'])
         return lunmap
 
-    def _get_ig(self, connector):
+    def _get_ig_name(self, connector):
         raise NotImplementedError()
 
     def create_consistencygroup(self, context, group):
@@ -615,6 +648,23 @@ class XtremIOVolumeDriver(san.SanDriver):
 
         return model_update, 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))
+
 
 class XtremIOISCSIDriver(XtremIOVolumeDriver, driver.ISCSIDriver):
     """Executes commands relating to ISCSI volumes.
@@ -637,69 +687,77 @@ class XtremIOISCSIDriver(XtremIOVolumeDriver, driver.ISCSIDriver):
         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['chap-discovery-initiator-user-name'] = 'chap_user'
+            discovery_passwd = self._get_password()
+            data['chap-discovery-initiator-password'] = discovery_passwd
+        return login_passwd, discovery_passwd
+
+    def _create_initiator(self, connector, login_chap, discovery_chap):
+        initiator = self._get_initiator_name(connector)
+        # 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)
-        use_chap = (sys.get('chap-authentication-mode', 'disabled') !=
-                    'disabled')
+        login_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.client.req('initiator-groups', 'GET',
-                                 name=self._get_ig(connector))['content']
-        except exception.NotFound:
-            # create an initiator group to hold the initiator
-            data = {'ig-name': self._get_ig(connector)}
-            self.client.req('initiator-groups', 'POST', data)
-            try:
-                ig = self.client.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.client.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.client.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.client.req('initiators', 'POST', data)
+        initiator_name = self._get_initiator_name(connector)
+        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'][1])
+        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 the initiator was created
+        if login_chap and not login_passwd:
+            LOG.info(_LI('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)
+        lunmap = self.create_lun_map(volume, ig['ig-id'][2])
 
         properties = self._get_iscsi_properties(lunmap)
 
-        if use_chap:
+        if login_chap:
             properties['auth_method'] = 'CHAP'
             properties['auth_username'] = 'chap_user'
-            properties['auth_password'] = chap_passwd
-
+            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', properties)
         return {
             'driver_volume_type': 'iscsi',
@@ -747,10 +805,10 @@ class XtremIOISCSIDriver(XtremIOVolumeDriver, driver.ISCSIDriver):
                       'target_luns': [lunmap['lun']] * len(portals)}
         return properties
 
-    def _get_initiator(self, connector):
+    def _get_initiator_name(self, connector):
         return connector['initiator']
 
-    def _get_ig(self, connector):
+    def _get_ig_name(self, connector):
         return connector['initiator']
 
 
@@ -780,40 +838,37 @@ class XtremIOFibreChannelDriver(XtremIOVolumeDriver,
 
     @fczm_utils.AddFCZone
     def initialize_connection(self, volume, connector):
-        initiators = self._get_initiator(connector)
-        ig_name = self._get_ig(connector)
+        wwpns = self._get_initiator_name(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
-        try:
-            # check if the IG already exists
-            ig = self.client.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.client.req('initiator-groups', 'POST', data)
-            try:
-                ig = self.client.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.client.req('initiators', name=initiator)['content']
-            except exception.NotFound:
-                # create an initiator
-                data = {'initiator-name': initiator,
-                        'ig-id': ig['name'],
-                        'port-address': initiator}
+        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)
-            i_t_map[initiator] = self.get_targets()
+        igs = list(set([i['ig-id'][1] for i in found] + [ig_name]))
 
-        lunmap = self.create_lun_map(volume, ig)
+        lun_num = None
+        for ig in igs:
+            lunmap = self.create_lun_map(volume, ig, lun_num)
+            lun_num = lunmap['lun']
         return {'driver_volume_type': 'fibre_channel',
                 'data': {
-                    'target_discovered': True,
-                    'target_lun': lunmap['lun'],
+                    'target_discovered': False,
+                    'target_lun': lun_num,
                     'target_wwn': self.get_targets(),
                     'access_mode': 'rw',
                     'initiator_target_map': i_t_map}}
@@ -822,21 +877,24 @@ class XtremIOFibreChannelDriver(XtremIOVolumeDriver,
     def terminate_connection(self, volume, connector, **kwargs):
         (super(XtremIOFibreChannelDriver, self)
          .terminate_connection(volume, connector, **kwargs))
-        num_vols = self.client.num_of_mapped_volumes(self._get_ig(connector))
+        num_vols = (self.client
+                    .num_of_mapped_volumes(self._get_ig_name(connector)))
         if num_vols > 0:
             data = {}
         else:
             i_t_map = {}
-            for initiator in self._get_initiator(connector):
-                i_t_map[initiator] = self.get_targets()
+            for initiator in self._get_initiator_name(connector):
+                i_t_map[initiator.replace(':', '')] = 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_initiator_name(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(self, connector):
+    def _get_ig_name(self, connector):
         return connector['host']