From cc9d728671de5daa9096203a1b7c23f032c54437 Mon Sep 17 00:00:00 2001 From: Peter Penchev Date: Mon, 12 Mar 2018 12:00:10 +0200 Subject: [PATCH] Add iSCSI export support to the StorPool driver. Add three new driver options: - iscsi_export_to: a list of IQN patterns that the driver should export volumes to using iSCSI and not the native StorPool protocol - iscsi_portal_group: the name of the iSCSI portal group defined in the StorPool configuration to use for these export - iscsi_learn_initiator_iqns: automatically create StorPool configuration records for an initiator when a volume is first exported to it Change-Id: I9de64306e0e6976268df782053b0651dd1cca96f --- .../unit/volume/drivers/test_storpool.py | 2 + cinder/volume/drivers/storpool.py | 282 +++++++++++++++++- 2 files changed, 283 insertions(+), 1 deletion(-) diff --git a/cinder/tests/unit/volume/drivers/test_storpool.py b/cinder/tests/unit/volume/drivers/test_storpool.py index a0797c3d1..a209f7e03 100644 --- a/cinder/tests/unit/volume/drivers/test_storpool.py +++ b/cinder/tests/unit/volume/drivers/test_storpool.py @@ -181,6 +181,8 @@ class StorPoolTestCase(test.TestCase): self.cfg.volume_backend_name = 'storpool_test' self.cfg.storpool_template = None self.cfg.storpool_replication = 3 + self.cfg.iscsi_export_to = '' + self.cfg.iscsi_portal_group = 'test-group' mock_exec = mock.Mock() mock_exec.return_value = ('', '') diff --git a/cinder/volume/drivers/storpool.py b/cinder/volume/drivers/storpool.py index 2c7c82c41..42423ecb3 100644 --- a/cinder/volume/drivers/storpool.py +++ b/cinder/volume/drivers/storpool.py @@ -17,6 +17,7 @@ from __future__ import absolute_import +import fnmatch import platform from oslo_config import cfg @@ -42,6 +43,17 @@ if storpool: storpool_opts = [ + cfg.StrOpt('iscsi_export_to', + default='iqn.1991-05.com.microsoft:*', + help='Export volumes via iSCSI to the hosts with IQNs that ' + 'match the patterns in this list.'), + cfg.BoolOpt('iscsi_learn_initiator_iqns', + default=True, + help='Create a StorPool record for a new initiator as soon as ' + 'Cinder asks for a volume to be exported to it.'), + cfg.StrOpt('iscsi_portal_group', + default=None, + help='The portal group to export volumes via iSCSI in.'), cfg.StrOpt('storpool_template', default=None, help='The StorPool template for volumes with no type.'), @@ -91,9 +103,10 @@ class StorPoolDriver(driver.VolumeDriver): 1.2.2 - Reintroduce the driver into OpenStack Queens, add ignore_errors to the internal _detach_volume() method 1.2.3 - Advertise some more driver capabilities. + 1.3.0 - Add support for exporting volumes via iSCSI. """ - VERSION = '1.2.3' + VERSION = '1.3.0' CI_WIKI_NAME = 'StorPool_CI' def __init__(self, *args, **kwargs): @@ -160,10 +173,264 @@ class StorPoolDriver(driver.VolumeDriver): raise StorPoolConfigurationInvalid( section=hostname, param='SP_OURID', error=e) + def _connector_wants_iscsi(self, connector): + """Should we do this export via iSCSI? + + Check the configuration to determine whether this connector is + expected to provide iSCSI exports as opposed to native StorPool + protocol ones. Match the initiator's IQN against the list of + patterns supplied in the "iscsi_export_to" configuration setting. + """ + try: + iqn = connector.get('initiator') + except Exception: + iqn = None + try: + host = connector.get('host') + except Exception: + host = None + if iqn is None or host is None: + LOG.debug(' - this connector certainly does not want iSCSI') + LOG.debug(' - check whether {} ({}) wants iSCSI'.format(host, iqn)) + export_to = self.configuration.iscsi_export_to + if export_to is None: + return False + for pat in export_to.split(): + LOG.debug(' - matching against {}'.format(pat)) + if fnmatch.fnmatch(iqn, pat): + LOG.debug(' - got it!') + return True + LOG.debug(' - nope') + return False + def validate_connector(self, connector): + if self._connector_wants_iscsi(connector): + return True return self._storpool_client_id(connector) >= 0 + def _get_iscsi_config(self, iqn, volume_id): + """Get the StorPool iSCSI config items pertaining to this volume. + + Find the elements of the StorPool iSCSI configuration tree that + will be needed to create, ensure, or remove the iSCSI export of + the specified volume to the specified initiator. + """ + cfg = self._attach.api().iSCSIConfig() + + pg_name = self.configuration.iscsi_portal_group + pg_found = [ + pg for pg in cfg.iscsi.portalGroups.values() if pg.name == pg_name + ] + if not pg_found: + raise Exception('StorPool Cinder iSCSI configuration error: ' + 'no portal group "{pg}"'.format(pg=pg_name)) + pg = pg_found[0] + + # Do we know about this initiator? + i_found = filter(lambda i: i.name == iqn, + cfg.iscsi.initiators.values()) + if i_found: + initiator = i_found[0] + else: + initiator = None + + # Is this volume already being exported? + volname = self._attach.volumeName(volume_id) + t_found = filter(lambda t: t.volume == volname, + cfg.iscsi.targets.values()) + if t_found: + target = t_found[0] + else: + target = None + + # OK, so is this volume being exported to this initiator? + export = None + if initiator is not None and target is not None: + e_found = [ + exp for exp in initiator.exports + if exp.portalGroup == pg.name and exp.target == target.name + ] + if e_found: + export = e_found[0] + + return { + 'cfg': cfg, + 'pg': pg, + 'initiator': initiator, + 'target': target, + 'export': export, + 'volume_name': volname, + 'volume_id': volume_id, + } + + def _create_iscsi_export(self, volume, connector): + """Create (if needed) an iSCSI export for the StorPool volume.""" + LOG.debug( + '_create_iscsi_export() invoked for volume "{}" ({}) connector {}' + .format(volume['display_name'], volume['id'], connector) + ) + iqn = connector['initiator'] + try: + cfg = self._get_iscsi_config(iqn, volume['id']) + except Exception as exc: + LOG.error( + 'Could not fetch the iSCSI config: {exc}'.format(exc=exc) + ) + raise + + if cfg['initiator'] is None: + if not self.configuration.iscsi_learn_initiator_iqns: + raise Exception('The "{iqn}" initiator IQN for the "{host}" ' + 'host is not defined in the StorPool ' + 'configuration.' + .format(iqn=iqn, host=connector['host'])) + else: + LOG.info('Creating a StorPool iSCSI initiator ' + 'for "{host}" ({iqn})' + .format(host=connector['host'], iqn=iqn)) + self._attach.api().iSCSIConfigChange({ + 'commands': [ + { + 'createInitiator': { + 'name': iqn, + 'username': '', + 'secret': '', + }, + }, + { + 'initiatorAddNetwork': { + 'initiator': iqn, + 'net': '0.0.0.0/0', + }, + }, + ] + }) + + if cfg['target'] is None: + LOG.info('Creating a StorPool iSCSI target ' + 'for the "{name}" volume ({id})' + .format(name=volume['display_name'], id=volume['id'])) + self._attach.api().iSCSIConfigChange({ + 'commands': [ + { + 'createTarget': { + 'volumeName': cfg['volume_name'], + }, + }, + ] + }) + cfg = self._get_iscsi_config(iqn, volume['id']) + + if cfg['export'] is None: + LOG.info('Creating a StorPool iSCSI export ' + 'for the "{name}" volume ({id}) ' + 'to the "{host}" initiator ({iqn}) ' + 'in the "{pg}" portal group' + .format(name=volume['display_name'], id=volume['id'], + host=connector['host'], iqn=iqn, + pg=cfg['pg'].name)) + self._attach.api().iSCSIConfigChange({ + 'commands': [ + { + 'export': { + 'initiator': iqn, + 'portalGroup': cfg['pg'].name, + 'volumeName': cfg['volume_name'], + }, + }, + ] + }) + + res = { + 'driver_volume_type': 'iscsi', + 'data': { + 'target_discovered': False, + 'target_iqn': cfg['target'].name, + 'target_portal': '{}:3260'.format( + cfg['pg'].networks[0].address + ), + 'target_lun': 0, + 'volume_id': volume['id'], + 'discard': True, + }, + } + LOG.debug('returning {}'.format(res)) + return res + + def _remove_iscsi_export(self, volume, connector): + """Remove an iSCSI export for the specified StorPool volume.""" + LOG.debug( + '_remove_iscsi_export() invoked for volume "{}" ({}) connector {}' + .format(volume['display_name'], volume['id'], connector) + ) + try: + cfg = self._get_iscsi_config(connector['initiator'], volume['id']) + except Exception as exc: + LOG.error( + 'Could not fetch the iSCSI config: {exc}'.format(exc=exc) + ) + raise + + if cfg['export'] is not None: + LOG.info('Removing the StorPool iSCSI export ' + 'for the "{name}" volume ({id}) ' + 'to the "{host}" initiator ({iqn}) ' + 'in the "{pg}" portal group' + .format(name=volume['display_name'], id=volume['id'], + host=connector['host'], + iqn=connector['initiator'], + pg=cfg['pg'].name)) + try: + self._attach.api().iSCSIConfigChange({ + 'commands': [ + { + 'exportDelete': { + 'initiator': cfg['initiator'].name, + 'portalGroup': cfg['pg'].name, + 'volumeName': cfg['volume_name'], + }, + }, + ] + }) + except spapi.ApiError as e: + if e.name not in ('objectExists', 'objectDoesNotExist'): + raise + LOG.info('Looks like somebody beat us to it') + + if cfg['target'] is not None: + last = True + for initiator in cfg['cfg'].iscsi.initiators.values(): + if initiator.name == cfg['initiator'].name: + continue + for exp in initiator.exports: + if exp.target == cfg['target'].name: + last = False + break + if not last: + break + + if last: + LOG.info('Removing the StorPool iSCSI target ' + 'for the "{name}" volume ({id})' + .format(name=volume['display_name'], id=volume['id'])) + try: + self._attach.api().iSCSIConfigChange({ + 'commands': [ + { + 'deleteTarget': { + 'volumeName': cfg['volume_name'], + }, + }, + ] + }) + except spapi.ApiError as e: + if e.name not in ('objectDoesNotExist', 'invalidParam'): + raise + LOG.info('Looks like somebody beat us to it') + def initialize_connection(self, volume, connector): + if self._connector_wants_iscsi(connector): + return self._create_iscsi_export(volume, connector) return {'driver_volume_type': 'storpool', 'data': { 'client_id': self._storpool_client_id(connector), @@ -171,6 +438,9 @@ class StorPoolDriver(driver.VolumeDriver): }} def terminate_connection(self, volume, connector, **kwargs): + if self._connector_wants_iscsi(connector): + LOG.debug('- removing an iSCSI export') + self._remove_iscsi_export(volume, connector) pass def create_snapshot(self, snapshot): @@ -222,6 +492,9 @@ class StorPoolDriver(driver.VolumeDriver): {'name': snapname, 'msg': e}) def create_export(self, context, volume, connector): + if self._connector_wants_iscsi(connector): + LOG.debug('- creating an iSCSI export') + self._create_iscsi_export(volume, connector) pass def remove_export(self, context, volume): @@ -263,6 +536,13 @@ class StorPoolDriver(driver.VolumeDriver): LOG.error("StorPoolDriver API initialization failed: %s", e) raise + export_to = self.configuration.iscsi_export_to + pg_name = self.configuration.iscsi_portal_group + if export_to is not None and export_to.split() and pg_name is None: + msg = _('The "iscsi_portal_group" option is required if ' + 'any patterns are listed in "iscsi_export_to"') + raise exception.VolumeDriverException(message=msg) + def get_volume_stats(self, refresh=False): if refresh: self._update_volume_stats() -- 2.24.0