From f78fa753800c9dc53fc56996f194d459029a8f3b Mon Sep 17 00:00:00 2001 From: Shay Halsband Date: Mon, 8 Jun 2015 19:08:46 +0300 Subject: [PATCH] XtremIO support for iscsi discovery auth - 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 | 4 + cinder/tests/unit/test_emc_xtremio.py | 70 +++++++ cinder/volume/drivers/emc/xtremio.py | 264 ++++++++++++++++---------- 3 files changed, 235 insertions(+), 103 deletions(-) diff --git a/cinder/exception.py b/cinder/exception.py index aef6c127e..2e312d8c4 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -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 " diff --git a/cinder/tests/unit/test_emc_xtremio.py b/cinder/tests/unit/test_emc_xtremio.py index 13f0b23a6..c8ca07e07 100644 --- a/cinder/tests/unit/test_emc_xtremio.py +++ b/cinder/tests/unit/test_emc_xtremio.py @@ -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): diff --git a/cinder/volume/drivers/emc/xtremio.py b/cinder/volume/drivers/emc/xtremio.py index 2e604419e..4a6a79b9c 100644 --- a/cinder/volume/drivers/emc/xtremio.py +++ b/cinder/volume/drivers/emc/xtremio.py @@ -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'] -- 2.45.2