From 8f87d144d022149fb8b87fcdc2ced30f9f2c34ee Mon Sep 17 00:00:00 2001
From: Peter Penchev <openstack-dev@storpool.com>
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             | 284 +++++++++++++++++-
 2 files changed, 285 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..074678227 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,266 @@ 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 = [
+            init for init in cfg.iscsi.initiators.values() if init.name == iqn
+        ]
+        if i_found:
+            initiator = i_found[0]
+        else:
+            initiator = None
+
+        # Is this volume already being exported?
+        volname = self._attach.volumeName(volume_id)
+        t_found = [
+            tgt for tgt in cfg.iscsi.targets.values() if tgt.volume == volname
+        ]
+        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 +440,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 +494,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 +538,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

