exp: re-add the iSCSI patch on top of it all
Change-Id: I76513a43161903efc83da40ca3656ae8e98dae52
diff --git a/patches/openstack/cinder/sep-sp-iscsi.patch b/patches/openstack/cinder/sep-sp-iscsi.patch
new file mode 100644
index 0000000..505e7ac
--- /dev/null
+++ b/patches/openstack/cinder/sep-sp-iscsi.patch
@@ -0,0 +1,647 @@
+From 6b37214585808aa06af0b1f303966bb22f47dc43 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:
+ - an empty string to use the StorPool native protocol for exporting volumes
+ protocol for exporting volumes)
+ - the string "*" to always use iSCSI for exporting volumes
+ - an experimental, not fully supported list of IQN patterns to export
+ volumes to using iSCSI; this results in a Cinder driver that exports
+ different volumes using different storage protocols
+- 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
+
+When exporting volumes via iSCSI, report the storage protocol as "iSCSI" and
+disable multiattach (the StorPool CI failures with iSCSI multiattach may need
+further investigation).
+
+Change-Id: I9de64306e0e6976268df782053b0651dd1cca96f
+---
+ .../unit/volume/drivers/test_storpool.py | 64 +++-
+ cinder/volume/drivers/storpool.py | 360 +++++++++++++++++-
+ .../drivers/storpool-volume-driver.rst | 49 ++-
+ 3 files changed, 464 insertions(+), 9 deletions(-)
+
+diff --git a/cinder/tests/unit/volume/drivers/test_storpool.py b/cinder/tests/unit/volume/drivers/test_storpool.py
+index 65d4ed304..350f8c9bd 100644
+--- a/cinder/tests/unit/volume/drivers/test_storpool.py
++++ b/cinder/tests/unit/volume/drivers/test_storpool.py
+@@ -32,6 +32,7 @@ fakeStorPool.sptypes = mock.Mock()
+ sys.modules['storpool'] = fakeStorPool
+
+
++from cinder.common import constants
+ from cinder import exception
+ from cinder.tests.unit import test
+ from cinder.volume import configuration as conf
+@@ -219,7 +220,14 @@ 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'
+
++ self._setup_test_driver()
++
++ def _setup_test_driver(self):
++ """Initialize a StorPool driver as per the current configuration."""
+ mock_exec = mock.Mock()
+ mock_exec.return_value = ('', '')
+
+@@ -228,7 +236,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),
+@@ -244,7 +252,7 @@ class StorPoolTestCase(test.TestCase):
+ conn)
+
+ @ddt.data(
+- (5, TypeError),
++ (5, (TypeError, AttributeError)),
+ ({'no-host': None}, KeyError),
+ ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
+ )
+@@ -635,3 +643,55 @@ class StorPoolTestCase(test.TestCase):
+ self.driver.get_pool({
+ 'volume_type': volume_type
+ }))
++
++ @ddt.data(
++ # The default values
++ ('', False, constants.STORPOOL, 'beleriand', False),
++
++ # Export to all
++ ('*', True, constants.ISCSI, 'beleriand', True),
++ ('*', True, constants.ISCSI, 'beleriand', True),
++
++ # Only export to the controller
++ ('', False, constants.STORPOOL, 'beleriand', False),
++
++ # Some of the not-fully-supported pattern lists
++ ('roh*', False, constants.STORPOOL, 'beleriand', False),
++ ('roh*', False, constants.STORPOOL, 'rohan', True),
++ ('*riand roh*', False, constants.STORPOOL, 'beleriand', True),
++ ('*riand roh*', False, constants.STORPOOL, 'rohan', True),
++ )
++ @ddt.unpack
++ def test_wants_iscsi(self, iscsi_export_to, use_iscsi, storage_protocol,
++ hostname, expected):
++ """Check the "should this export use iSCSI?" detection."""
++ self.cfg.iscsi_export_to = iscsi_export_to
++ self._setup_test_driver()
++ self.assertEqual(self.driver._use_iscsi, use_iscsi)
++
++ # Make sure the driver reports the correct protocol in the stats
++ self.driver._update_volume_stats()
++ self.assertEqual(self.driver._stats["vendor_name"], "StorPool")
++ self.assertEqual(self.driver._stats["storage_protocol"],
++ storage_protocol)
++
++ def check(conn, forced, expected):
++ """Pass partially or completely valid connector info."""
++ for initiator in (None, hostname):
++ for host in (None, 'gondor'):
++ self.assertEqual(
++ self.driver._connector_wants_iscsi({
++ "host": host,
++ "initiator": initiator,
++ **conn,
++ }),
++ expected if initiator is not None and host is not None
++ else forced)
++
++ # If iscsi_cinder_volume is set and this is the controller, then yes.
++ check({"storpool_wants_iscsi": True}, True, True)
++
++ # If iscsi_cinder_volume is not set or this is not the controller, then
++ # look at the specified expected value.
++ check({"storpool_wants_iscsi": False}, use_iscsi, expected)
++ check({}, use_iscsi, expected)
+diff --git a/cinder/volume/drivers/storpool.py b/cinder/volume/drivers/storpool.py
+index 418e5750f..abb23865d 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
+@@ -44,6 +45,31 @@ 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='Whether to export volumes using iSCSI. '
++ 'An empty string (the default) makes the driver export '
++ 'all volumes using the StorPool native network protocol. '
++ 'The value "*" makes the driver export all volumes using '
++ 'iSCSI. '
++ 'Any other value leads to an experimental not fully '
++ 'supported configuration and is interpreted as '
++ 'a whitespace-separated list of patterns for IQNs for '
++ 'hosts that need volumes to be exported via iSCSI, 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.'),
+@@ -105,6 +131,7 @@ class StorPoolDriver(driver.VolumeDriver):
+ self._ourId = None
+ self._ourIdInt = None
+ self._attach = None
++ self._use_iscsi = None
+
+ @staticmethod
+ def get_driver_options():
+@@ -162,10 +189,312 @@ 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 self._use_iscsi:
++ LOG.debug(' - forcing iSCSI for all exported volumes')
++ return True
++ 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:
++ 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),
+@@ -174,6 +503,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):
+@@ -275,11 +607,20 @@ class StorPoolDriver(driver.VolumeDriver):
+ )
+
+ 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.configuration.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:
+@@ -316,6 +657,17 @@ 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)
++
++ self._use_iscsi = export_to == "*"
++
+ def _update_volume_stats(self):
+ try:
+ dl = self._attach.api().disksList()
+@@ -341,7 +693,7 @@ class StorPoolDriver(driver.VolumeDriver):
+ 'total_capacity_gb': total / units.Gi,
+ 'free_capacity_gb': free / units.Gi,
+ 'reserved_percentage': 0,
+- 'multiattach': True,
++ 'multiattach': not self._use_iscsi,
+ 'QoS_support': False,
+ 'thick_provisioning_support': False,
+ 'thin_provisioning_support': True,
+@@ -359,7 +711,9 @@ class StorPoolDriver(driver.VolumeDriver):
+ 'volume_backend_name') or 'storpool',
+ 'vendor_name': 'StorPool',
+ 'driver_version': self.VERSION,
+- 'storage_protocol': constants.STORPOOL,
++ 'storage_protocol': (
++ constants.ISCSI if self._use_iscsi else constants.STORPOOL
++ ),
+
+ 'clone_across_pools': True,
+ 'sparse_copy_volume': True,
+diff --git a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
+index d2c5895a9..c891675bc 100644
+--- a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
++++ b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
+@@ -19,12 +19,15 @@ Prerequisites
+ * The controller and all the compute nodes must have access to the StorPool
+ API service.
+
+-* All nodes where StorPool-backed volumes will be attached must have access to
++* If iSCSI is not being used as a transport (see below), all nodes where
++ StorPool-backed volumes will be attached must have access to
+ the StorPool data network and run the ``storpool_block`` service.
+
+-* If StorPool-backed Cinder volumes need to be created directly from Glance
+- images, then the node running the ``cinder-volume`` service must also have
+- access to the StorPool data network and run the ``storpool_block`` service.
++* If Glance uses Cinder as its image store, or if StorPool-backed Cinder
++ volumes need to be created directly from Glance images, and iSCSI is not
++ being used as a transport, then the node running the ``cinder-volume``
++ service must also have access to the StorPool data network and run
++ the ``storpool_block`` service.
+
+ * All nodes that need to access the StorPool API (the compute nodes and
+ the node running the ``cinder-volume`` service) must have the following
+@@ -34,6 +37,29 @@ Prerequisites
+ * the storpool Python bindings package
+ * the storpool.spopenstack Python helper package
+
++Using iSCSI as the transport protocol
++-------------------------------------
++
++The StorPool distributed storage system uses its own, highly optimized and
++tailored for its specifics, network protocol for communication between
++the storage servers and the clients (the OpenStack cluster nodes where
++StorPool-backed volumes will be attached). There are cases when granting
++various nodes access to the StorPool data network or installing and
++running the ``storpool_block`` client service on them may pose difficulties.
++The StorPool servers may also expose the user-created volumes and snapshots
++using the standard iSCSI protocol that only requires TCP routing and
++connectivity between the storage servers and the StorPool clients.
++The StorPool Cinder driver may be configured to export volumes and
++snapshots via iSCSI using the ``iscsi_export_to`` and ``iscsi_portal_group``
++configuration options.
++
++Additionally, even if e.g. the hypervisor nodes running Nova will use
++the StorPool network protocol and run the ``storpool_block`` service
++(so the ``iscsi_export_to`` option has its default empty string value),
++the ``iscsi_cinder_volume`` option configures the StorPool Cinder driver
++so that only the ``cinder-volume`` service will use the iSCSI protocol when
++attaching volumes and snapshots to transfer data to and from Glance images.
++
+ Configuring the StorPool volume driver
+ --------------------------------------
+
+@@ -55,6 +81,21 @@ volume backend definition) and per volume type:
+ with the default placement constraints for the StorPool cluster.
+ The default value for the chain replication is 3.
+
++- ``iscsi_export_to``: if set to the value ``*``, the StorPool Cinder driver
++ will export volumes and snapshots using the iSCSI protocol instead of
++ the StorPool network protocol. The ``iscsi_portal_group`` option must also
++ be specified.
++
++- ``iscsi_portal_group``: if the ``iscsi_export_to`` option is set to
++ the value ``*`` or the ``iscsi_cinder_volume`` option is turned on,
++ this option specifies the name of the iSCSI portal group that Cinder
++ volumes will be exported to.
++
++- ``iscsi_cinder_volume``: if enabled, even if the ``iscsi_export_to`` option
++ has its default empty value, the ``cinder-volume`` service will use iSCSI
++ to attach the volumes and snapshots for transferring data to and from
++ Glance images.
++
+ Using the StorPool volume driver
+ --------------------------------
+
+--
+2.35.1
+