exp: add the iSCSI and "no backup volume" patches.

Change-Id: Iff34a5096c9ec0ceb0d5884438ebc6f9fbe8523e
diff --git a/patches/openstack/cinder/storpool-iscsi.patch b/patches/openstack/cinder/storpool-iscsi.patch
new file mode 100644
index 0000000..485d5b5
--- /dev/null
+++ b/patches/openstack/cinder/storpool-iscsi.patch
@@ -0,0 +1,474 @@
+From ccf22ebfa2a2a19469f791c7624c5c28bab3b928 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 four new driver options:
+- iscsi_cinder_volume: use StorPool iSCSI attachments whenever
+  the cinder-volume service needs to attach a volume to the controller,
+  e.g. for copying an image to a volume or vice versa
+- 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      |   7 +-
+ cinder/volume/drivers/storpool.py             | 354 +++++++++++++++++-
+ 2 files changed, 357 insertions(+), 4 deletions(-)
+
+diff --git a/cinder/tests/unit/volume/drivers/test_storpool.py b/cinder/tests/unit/volume/drivers/test_storpool.py
+index 843283db4..df57dee5d 100644
+--- a/cinder/tests/unit/volume/drivers/test_storpool.py
++++ b/cinder/tests/unit/volume/drivers/test_storpool.py
+@@ -181,6 +181,9 @@ class StorPoolTestCase(test.TestCase):
+         self.cfg.volume_backend_name = 'storpool_test'
+         self.cfg.storpool_template = None
+         self.cfg.storpool_replication = 3
++        self.cfg.iscsi_cinder_volume = False
++        self.cfg.iscsi_export_to = ''
++        self.cfg.iscsi_portal_group = 'test-group'
+ 
+         mock_exec = mock.Mock()
+         mock_exec.return_value = ('', '')
+@@ -190,7 +193,7 @@ class StorPoolTestCase(test.TestCase):
+         self.driver.check_for_setup_error()
+ 
+     @ddt.data(
+-        (5, TypeError),
++        (5, (TypeError, AttributeError)),
+         ({'no-host': None}, KeyError),
+         ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
+         ({'host': 's01'}, None),
+@@ -206,7 +209,7 @@ class StorPoolTestCase(test.TestCase):
+                               conn)
+ 
+     @ddt.data(
+-        (5, TypeError),
++        (5, (TypeError, AttributeError)),
+         ({'no-host': None}, KeyError),
+         ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
+     )
+diff --git a/cinder/volume/drivers/storpool.py b/cinder/volume/drivers/storpool.py
+index 0d2903684..b4e6ef9e4 100644
+--- a/cinder/volume/drivers/storpool.py
++++ b/cinder/volume/drivers/storpool.py
+@@ -15,6 +15,7 @@
+ 
+ """StorPool block device driver"""
+ 
++import fnmatch
+ import platform
+ 
+ from oslo_config import cfg
+@@ -41,6 +42,24 @@ if storpool:
+ 
+ 
+ storpool_opts = [
++    cfg.BoolOpt('iscsi_cinder_volume',
++                default=False,
++                help='Let the cinder-volume service use iSCSI instead of '
++                     'the StorPool block device driver for accessing '
++                     'StorPool volumes, e.g. when creating a volume from '
++                     'an image or vice versa.'),
++    cfg.StrOpt('iscsi_export_to',
++               default='',
++               help='Export volumes via iSCSI to the hosts with IQNs that '
++                    'match the patterns in this list, e.g. '
++                    '"iqn.1991-05.com.microsoft:*" for Windows hosts'),
++    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.'),
+@@ -92,9 +111,10 @@ class StorPoolDriver(driver.VolumeDriver):
+         1.2.3   - Advertise some more driver capabilities.
+         2.0.0   - Drop _attach_volume() and _detach_volume(), our os-brick
+                   connector will handle this.
++                - Add support for exporting volumes via iSCSI.
+     """
+ 
+-    VERSION = '1.2.3'
++    VERSION = '2.0.0'
+     CI_WIKI_NAME = 'StorPool_distributed_storage_CI'
+ 
+     def __init__(self, *args, **kwargs):
+@@ -161,10 +181,319 @@ 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.
++        """
++        if connector is None:
++            return False
++        if connector.get('storpool_wants_iscsi'):
++            LOG.debug('  - forcing iSCSI for the controller')
++            return True
++
++        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')
++            return False
++
++        LOG.debug('  - check whether %(host)s (%(iqn)s) wants iSCSI',
++                  {
++                      'host': host,
++                      'iqn': 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 %(pat)s', {'pat': 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 '
++            '"%(vol_name)s" (%(vol_id)s) connector %(connector)s',
++            {
++                'vol_name': volume['display_name'],
++                'vol_id': volume['id'],
++                'connector': 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)s', {'exc': exc}
++            )
++            raise
++
++        if cfg['initiator'] is None:
++            LOG.info(
++                'RDBG initiator? learn %(learn)s vol %(vol)s want %(want)s',
++                {
++                    'learn': repr(
++                        self.configuration.iscsi_learn_initiator_iqns
++                    ),
++                    'vol': repr(self.configuration.iscsi_cinder_volume),
++                    'want': repr(connector.get('storpool_wants_iscsi')),
++                }
++            )
++            if not (self.configuration.iscsi_learn_initiator_iqns or
++                    self.configuration.iscsi_cinder_volume and
++                    connector.get('storpool_wants_iscsi')):
++                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}s" ({iqn}s)',
++                         {'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 "%(vol_name)s" volume (%(vol_id)s)',
++                {
++                    'vol_name': volume['display_name'],
++                    'vol_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 "{vol_name}s" volume ({vol_id}s) '
++                     'to the "{host}s" initiator ({iqn}s) '
++                     'in the "{pg}s" portal group',
++                     {
++                         'vol_name': volume['display_name'],
++                         'vol_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 %(res)s', {'res': 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 '
++            '"%(vol_name)s" (%(vol_id)s) connector %(conn)s',
++            {
++                'vol_name': volume['display_name'],
++                'vol_id': volume['id'],
++                'conn': connector,
++            }
++        )
++        try:
++            cfg = self._get_iscsi_config(connector['initiator'], volume['id'])
++        except Exception as exc:
++            LOG.error(
++                'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
++            )
++            raise
++
++        if cfg['export'] is not None:
++            LOG.info('Removing the StorPool iSCSI export '
++                     'for the "%(vol_name)s" volume (%(vol_id)s) '
++                     'to the "%(host)s" initiator (%(iqn)s) '
++                     'in the "%(pg)s" portal group',
++                     {
++                         'vol_name': volume['display_name'],
++                         'vol_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 "{vol_name}s" volume ({vol_id}s)',
++                    {
++                        'vol_name': volume['display_name'],
++                        'vol_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),
+@@ -173,6 +502,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):
+@@ -224,11 +556,20 @@ class StorPoolDriver(driver.VolumeDriver):
+                           {'name': snapname, 'msg': e})
+ 
+     def create_export(self, context, volume, connector):
+-        pass
++        if self._connector_wants_iscsi(connector):
++            LOG.debug('- creating an iSCSI export')
++            self._create_iscsi_export(volume, connector)
+ 
+     def remove_export(self, context, volume):
+         pass
+ 
++    def _attach_volume(self, context, volume, properties, remote=False):
++        if self.iscsi_cinder_volume and not remote:
++            LOG.debug('- adding the "storpool_wants_iscsi" flag')
++            properties['storpool_wants_iscsi'] = True
++
++        return super()._attach_volume(context, volume, properties, remote)
++
+     def delete_volume(self, volume):
+         name = self._attach.volumeName(volume['id'])
+         try:
+@@ -265,6 +606,15 @@ class StorPoolDriver(driver.VolumeDriver):
+             LOG.error("StorPoolDriver API initialization failed: %s", e)
+             raise
+ 
++        export_to = self.configuration.iscsi_export_to
++        export_to_set = export_to is not None and export_to.split()
++        vol_iscsi = self.configuration.iscsi_cinder_volume
++        pg_name = self.configuration.iscsi_portal_group
++        if (export_to_set or vol_iscsi) 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 _update_volume_stats(self):
+         try:
+             dl = self._attach.api().disksList()
+-- 
+2.33.0
+