Regenerate the iSCSI patch

Change-Id: I3bc45d1218f4fe1fbe8086e7daa15af5aeb8ad5a
diff --git a/patches/openstack/cinder/sep-sp-iscsi.patch b/patches/openstack/cinder/sep-sp-iscsi.patch
index 1ec0908..a7dbea7 100644
--- a/patches/openstack/cinder/sep-sp-iscsi.patch
+++ b/patches/openstack/cinder/sep-sp-iscsi.patch
@@ -1,4 +1,4 @@
-From 2acfdddf0794754aa0e32a56800e77387f75ce38 Mon Sep 17 00:00:00 2001
+From f15732ac9c326996b418aaaf40aece68912f038b 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
@@ -25,118 +25,66 @@
 
 Change-Id: I9de64306e0e6976268df782053b0651dd1cca96f
 ---
- .../unit/volume/drivers/test_storpool.py      | 521 +++++++++++++++++-
- cinder/volume/drivers/storpool.py             | 379 ++++++++++++-
+ .../unit/volume/drivers/test_storpool.py      | 441 +++++++++++++++++-
+ cinder/volume/drivers/storpool.py             | 381 ++++++++++++++-
  .../drivers/storpool-volume-driver.rst        |  68 ++-
  .../storpool-iscsi-cefcfe590a07c5c7.yaml      |  15 +
- 4 files changed, 972 insertions(+), 11 deletions(-)
+ 4 files changed, 889 insertions(+), 16 deletions(-)
  create mode 100644 releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
 
 diff --git a/cinder/tests/unit/volume/drivers/test_storpool.py b/cinder/tests/unit/volume/drivers/test_storpool.py
-index 44707d0b8..d6347e1d5 100644
+index 2015c734d..440740a56 100644
 --- a/cinder/tests/unit/volume/drivers/test_storpool.py
 +++ b/cinder/tests/unit/volume/drivers/test_storpool.py
-@@ -14,14 +14,24 @@
+@@ -13,9 +13,10 @@
+ #    License for the specific language governing permissions and limitations
  #    under the License.
  
- 
-+from __future__ import annotations
-+
+-
 +import dataclasses
  import itertools
  import re
- import sys
 +from typing import Any, NamedTuple, TYPE_CHECKING  # noqa: H301
  from unittest import mock
  
  import ddt
+@@ -23,6 +24,7 @@ from os_brick.initiator import storpool_utils
+ from os_brick.tests.initiator import test_storpool_utils
  from oslo_utils import units
  
-+if TYPE_CHECKING:
-+    if sys.version_info >= (3, 11):
-+        from typing import Self
-+    else:
-+        from typing_extensions import Self
-+
- 
- fakeStorPool = mock.Mock()
- fakeStorPool.spopenstack = mock.Mock()
-@@ -31,6 +41,7 @@ fakeStorPool.sptypes = mock.Mock()
- sys.modules['storpool'] = fakeStorPool
- 
- 
 +from cinder.common import constants
  from cinder import exception
  from cinder.tests.unit import fake_constants
  from cinder.tests.unit import test
-@@ -38,6 +49,13 @@ from cinder.volume import configuration as conf
+@@ -30,6 +32,13 @@ from cinder.volume import configuration as conf
  from cinder.volume.drivers import storpool as driver
  
  
-+_ISCSI_IQN_OURS = 'beleriand'
-+_ISCSI_IQN_OTHER = 'rohan'
-+_ISCSI_IQN_THIRD = 'gondor'
-+_ISCSI_PAT_OTHER = 'roh*'
-+_ISCSI_PAT_BOTH = '*riand roh*'
-+_ISCSI_PORTAL_GROUP = 'openstack_pg'
++ISCSI_IQN_OURS = 'beleriand'
++ISCSI_IQN_OTHER = 'rohan'
++ISCSI_IQN_THIRD = 'gondor'
++ISCSI_PAT_OTHER = 'roh*'
++ISCSI_PAT_BOTH = '*riand roh*'
++ISCSI_PORTAL_GROUP = 'openstack_pg'
 +
  volume_types = {
      fake_constants.VOLUME_TYPE_ID: {},
      fake_constants.VOLUME_TYPE2_ID: {'storpool_template': 'ssd'},
-@@ -71,6 +89,10 @@ def snapshotName(vtype, vid):
-     return 'os--snap--{t}--{id}'.format(t=vtype, id=vid)
+@@ -67,6 +76,10 @@ def snapshotName(vtype, vid, more=None):
+     )
  
  
 +def targetName(vid):
 +    return 'iqn.2012-11.storpool:{id}'.format(id=vid)
 +
 +
- class MockDisk(object):
-     def __init__(self, diskId):
-         self.id = diskId
-@@ -195,6 +217,315 @@ def MockVolumeUpdateDesc(size):
-     return {'size': size}
+ class MockAPI(object):
+     def __init__(self, *args):
+         self._disks = {}
+@@ -162,6 +175,242 @@ class MockAPI(object):
+         volumes[name] = dict(snapshots[snapname])
  
  
-+@dataclasses.dataclass(frozen=True)
-+class MockIscsiNetwork:
-+    """Mock a StorPool IP CIDR network definition (partially)."""
-+
-+    address: str
-+
-+
-+@dataclasses.dataclass(frozen=True)
-+class MockIscsiPortalGroup:
-+    """Mock a StorPool iSCSI portal group definition (partially)."""
-+
-+    name: str
-+    networks: list[MockIscsiNetwork]
-+
-+
-+@dataclasses.dataclass(frozen=True)
-+class MockIscsiExport:
-+    """Mock a StorPool iSCSI exported volume/target definition."""
-+
-+    portalGroup: str
-+    target: str
-+
-+
-+@dataclasses.dataclass(frozen=True)
-+class MockIscsiInitiator:
-+    """Mock a StorPool iSCSI initiator definition."""
-+
-+    name: str
-+    exports: list[MockIscsiExport]
-+
-+
-+@dataclasses.dataclass(frozen=True)
-+class MockIscsiTarget:
-+    """Mock a StorPool iSCSI volume-to-target mapping definition."""
-+
-+    name: str
-+    volume: str
-+
-+
 +class IscsiTestCase(NamedTuple):
 +    """A single test case for the iSCSI config and export methods."""
 +
@@ -150,224 +98,190 @@
 +class MockIscsiConfig:
 +    """Mock the structure returned by the "get current config" query."""
 +
-+    portalGroups: dict[str, MockIscsiPortalGroup]
-+    initiators: dict[str, MockIscsiInitiator]
-+    targets: dict[str, MockIscsiTarget]
-+
 +    @classmethod
-+    def build(cls, tcase: IscsiTestCase) -> Self:
++    def build(cls, tcase: IscsiTestCase) -> dict:
 +        """Build a test config structure."""
 +        initiators = {
-+            '0': MockIscsiInitiator(name=_ISCSI_IQN_OTHER, exports=[]),
++            '0': {'name': ISCSI_IQN_OTHER, 'exports': []},
 +        }
 +        if tcase.initiator is not None:
-+            initiators['1'] = MockIscsiInitiator(
-+                name=tcase.initiator,
-+                exports=(
++            initiators['1'] = {
++                'name': tcase.initiator,
++                'exports': (
 +                    [
-+                        MockIscsiExport(
-+                            portalGroup=_ISCSI_PORTAL_GROUP,
-+                            target=targetName(tcase.volume),
-+                        ),
++                        {
++                            'portalGroup': ISCSI_PORTAL_GROUP,
++                            'target': targetName(tcase.volume),
++                        },
 +                    ]
 +                    if tcase.exported
 +                    else []
 +                ),
-+            )
++            }
 +
 +        targets = {
-+            '0': MockIscsiTarget(
-+                name=targetName(fake_constants.VOLUME2_ID),
-+                volume=volumeName(fake_constants.VOLUME2_ID),
-+            ),
++            '0': {
++                'name': targetName(fake_constants.VOLUME2_ID),
++                'volume': volumeName(fake_constants.VOLUME2_ID),
++            },
 +        }
 +        if tcase.volume is not None:
-+            targets['1'] = MockIscsiTarget(
-+                name=targetName(tcase.volume),
-+                volume=volumeName(tcase.volume),
-+            )
++            targets['1'] = {
++                'name': targetName(tcase.volume),
++                'volume': volumeName(tcase.volume),
++            }
 +
-+        return cls(
-+            portalGroups={
-+                '0': MockIscsiPortalGroup(
-+                    name=_ISCSI_PORTAL_GROUP + '-not',
-+                    networks=[],
-+                ),
-+                '1': MockIscsiPortalGroup(
-+                    name=_ISCSI_PORTAL_GROUP,
-+                    networks=[
-+                        MockIscsiNetwork(address="192.0.2.0"),
-+                        MockIscsiNetwork(address="195.51.100.0"),
++        return {
++
++            'portalGroups': {
++                '0': {
++                    'name': ISCSI_PORTAL_GROUP + '-not',
++                    'networks': [],
++                },
++                '1': {
++                    'name': ISCSI_PORTAL_GROUP,
++                    'networks': [
++                        {'address': "192.0.2.0"},
++                        {'address': "195.51.100.0"},
 +                    ],
-+                ),
++                },
 +            },
-+            initiators=initiators,
-+            targets=targets,
-+        )
++            'initiators': initiators,
++            'targets': targets,
 +
++        }
 +
-+@dataclasses.dataclass(frozen=True)
-+class MockIscsiConfigTop:
-+    """Mock the top level of the "get the iSCSI configuration" response."""
-+
-+    iscsi: MockIscsiConfig
 +
 +
 +class MockIscsiAPI:
 +    """Mock only the iSCSI-related calls of the StorPool API bindings."""
 +
 +    _asrt: test.TestCase
-+    _configs: list[MockIscsiConfig]
++    _configs: list[dict]
 +
 +    def __init__(
 +        self,
-+        configs: list[MockIscsiConfig],
++        configs: list[dict],
 +        asrt: test.TestCase,
 +    ) -> None:
 +        """Store the reference to the list of iSCSI config objects."""
 +        self._asrt = asrt
 +        self._configs = configs
 +
-+    def iSCSIConfig(self) -> MockIscsiConfigTop:
++    def get_iscsi_config(self) -> dict:
 +        """Return the last version of the iSCSI configuration."""
-+        return MockIscsiConfigTop(iscsi=self._configs[-1])
++        return {'iscsi': self._configs[-1]}
 +
-+    def _handle_export(
-+        self,
-+        cfg: MockIscsiConfig, cmd: dict[str, Any],
-+    ) -> MockIscsiConfig:
++    def _handle_export(self, cfg: dict, cmd: dict[str, Any]) -> dict:
 +        """Add an export for an initiator."""
 +        self._asrt.assertDictEqual(
 +            cmd,
 +            {
-+                'initiator': _ISCSI_IQN_OURS,
-+                'portalGroup': _ISCSI_PORTAL_GROUP,
++                'initiator': ISCSI_IQN_OURS,
++                'portalGroup': ISCSI_PORTAL_GROUP,
 +                'volumeName': volumeName(fake_constants.VOLUME_ID),
 +            },
 +        )
-+        self._asrt.assertEqual(cfg.initiators['1'].name, cmd['initiator'])
-+        self._asrt.assertListEqual(cfg.initiators['1'].exports, [])
++        self._asrt.assertEqual(cfg['initiators']['1']['name'], cmd['initiator'])
++        self._asrt.assertListEqual(cfg['initiators']['1']['exports'], [])
 +
-+        return dataclasses.replace(
-+            cfg,
-+            initiators={
-+                **cfg.initiators,
-+                '1': dataclasses.replace(
-+                    cfg.initiators['1'],
-+                    exports=[
-+                        MockIscsiExport(
-+                            portalGroup=cmd['portalGroup'],
-+                            target=targetName(fake_constants.VOLUME_ID),
-+                        ),
-+                    ],
-+                ),
++        cfg['initiators'] = {
++            **cfg['initiators'],
++            '1': {
++                **cfg['initiators']['1'],
++                'exports': [
++                    {
++                        'portalGroup': cmd['portalGroup'],
++                        'target': targetName(fake_constants.VOLUME_ID),
++                    },
++                ],
 +            },
-+        )
++        }
++        return cfg
 +
-+    def _handle_delete_export(
-+        self,
-+        cfg: MockIscsiConfig,
-+        cmd: dict[str, Any],
-+    ) -> MockIscsiConfig:
++    def _handle_delete_export(self, cfg: dict, cmd: dict[str, Any]) -> dict:
 +        """Delete an export for an initiator."""
 +        self._asrt.assertDictEqual(
 +            cmd,
 +            {
-+                'initiator': _ISCSI_IQN_OURS,
-+                'portalGroup': _ISCSI_PORTAL_GROUP,
++                'initiator': ISCSI_IQN_OURS,
++                'portalGroup': ISCSI_PORTAL_GROUP,
 +                'volumeName': volumeName(fake_constants.VOLUME_ID),
 +            },
 +        )
-+        self._asrt.assertEqual(cfg.initiators['1'].name, cmd['initiator'])
++        self._asrt.assertEqual(cfg['initiators']['1']['name'], cmd['initiator'])
 +        self._asrt.assertListEqual(
-+            cfg.initiators['1'].exports,
-+            [MockIscsiExport(portalGroup=_ISCSI_PORTAL_GROUP,
-+                             target=cfg.targets['1'].name)])
++            cfg['initiators']['1']['exports'],
++            [{'portalGroup': ISCSI_PORTAL_GROUP,
++                             'target': cfg['targets']['1']['name']}])
 +
-+        updated_initiators = cfg.initiators
-+        del updated_initiators['1']
-+        return dataclasses.replace(cfg, initiators=updated_initiators)
++        del cfg['initiators']['1']
++        return cfg
 +
-+    def _handle_create_initiator(
-+        self,
-+        cfg: MockIscsiConfig,
-+        cmd: dict[str, Any],
-+    ) -> MockIscsiConfig:
++    def _handle_create_initiator(self, cfg: dict, cmd: dict[str, Any]) -> dict:
 +        """Add a whole new initiator."""
 +        self._asrt.assertDictEqual(
 +            cmd,
 +            {
-+                'name': _ISCSI_IQN_OURS,
++                'name': ISCSI_IQN_OURS,
 +                'username': '',
 +                'secret': '',
 +            },
 +        )
 +        self._asrt.assertNotIn(
 +            cmd['name'],
-+            [init.name for init in cfg.initiators.values()],
++            [init['name'] for init in cfg['initiators'].values()],
 +        )
-+        self._asrt.assertListEqual(sorted(cfg.initiators), ['0'])
++        self._asrt.assertListEqual(sorted(cfg['initiators']), ['0'])
 +
-+        return dataclasses.replace(
-+            cfg,
-+            initiators={
-+                **cfg.initiators,
-+                '1': MockIscsiInitiator(name=cmd['name'], exports=[]),
-+            },
-+        )
++        cfg['initiators'] = {
++            **cfg['initiators'],
++            '1': {'name': cmd['name'], 'exports': []},
++        }
++        return cfg
 +
-+    def _handle_create_target(
-+        self,
-+        cfg: MockIscsiConfig,
-+        cmd: dict[str, Any],
-+    ) -> MockIscsiConfig:
++
++    def _handle_create_target(self, cfg: dict, cmd: dict[str, Any]) -> dict:
 +        """Add a target for a volume so that it may be exported."""
 +        self._asrt.assertDictEqual(
 +            cmd,
 +            {'volumeName': volumeName(fake_constants.VOLUME_ID)},
 +        )
-+        self._asrt.assertListEqual(sorted(cfg.targets), ['0'])
-+        return dataclasses.replace(
-+            cfg,
-+            targets={
-+                **cfg.targets,
-+                '1': MockIscsiTarget(
-+                    name=targetName(fake_constants.VOLUME_ID),
-+                    volume=volumeName(fake_constants.VOLUME_ID),
-+                ),
++        self._asrt.assertListEqual(sorted(cfg['targets']), ['0'])
++        cfg['targets'] = {
++            **cfg['targets'],
++            '1': {
++                'name': targetName(fake_constants.VOLUME_ID),
++                'volume': volumeName(fake_constants.VOLUME_ID),
 +            },
-+        )
++        }
++        return cfg
 +
-+    def _handle_delete_target(
-+        self,
-+        cfg: MockIscsiConfig,
-+        cmd: dict[str, Any]
-+    ) -> MockIscsiConfig:
++    def _handle_delete_target(self, cfg: dict, cmd: dict[str, Any]) -> dict:
 +        """Remove a target for a volume."""
 +        self._asrt.assertDictEqual(
 +            cmd,
 +            {'volumeName': volumeName(fake_constants.VOLUME_ID)},
 +        )
 +
-+        self._asrt.assertListEqual(sorted(cfg.targets), ['0', '1'])
-+        updated_targets = cfg.targets
-+        del updated_targets['1']
-+        return dataclasses.replace(cfg, targets=updated_targets)
++        self._asrt.assertListEqual(sorted(cfg['targets']), ['0', '1'])
++        del cfg['targets']['1']
++        return cfg
 +
 +    def _handle_initiator_add_network(
 +        self,
-+        cfg: MockIscsiConfig,
++        cfg: dict,
 +        cmd: dict[str, Any],
-+    ) -> MockIscsiConfig:
++    ) -> dict:
 +        """Add a network that an initiator is allowed to log in from."""
 +        self._asrt.assertDictEqual(
 +            cmd,
 +            {
-+                'initiator': _ISCSI_IQN_OURS,
++                'initiator': ISCSI_IQN_OURS,
 +                'net': '0.0.0.0/0',
 +            },
 +        )
-+        return dataclasses.replace(cfg)
++        return cfg
 +
 +    _CMD_HANDLERS = {
 +        'createInitiator': _handle_create_initiator,
@@ -378,7 +292,7 @@
 +        'initiatorAddNetwork': _handle_initiator_add_network,
 +    }
 +
-+    def iSCSIConfigChange(
++    def post_iscsi_config(
 +        self,
 +        commands: dict[str, list[dict[str, dict[str, Any]]]],
 +    ) -> None:
@@ -401,33 +315,33 @@
 +
 +_ISCSI_TEST_CASES = [
 +    IscsiTestCase(None, None, False, 4),
-+    IscsiTestCase(_ISCSI_IQN_OURS, None, False, 2),
-+    IscsiTestCase(_ISCSI_IQN_OURS, fake_constants.VOLUME_ID, False, 1),
-+    IscsiTestCase(_ISCSI_IQN_OURS, fake_constants.VOLUME_ID, True, 0),
++    IscsiTestCase(ISCSI_IQN_OURS, None, False, 2),
++    IscsiTestCase(ISCSI_IQN_OURS, fake_constants.VOLUME_ID, False, 1),
++    IscsiTestCase(ISCSI_IQN_OURS, fake_constants.VOLUME_ID, True, 0),
 +]
 +
 +
- def MockSPConfig(section = 's01'):
-     res = {}
-     m = re.match('^s0*([A-Za-z0-9]+)$', section)
-@@ -237,7 +568,15 @@ class StorPoolTestCase(test.TestCase):
+ class MockVolumeDB(object):
+     """Simulate a Cinder database with a volume_get() method."""
+ 
+@@ -198,7 +447,15 @@ class StorPoolTestCase(test.TestCase):
          self.cfg.volume_backend_name = 'storpool_test'
          self.cfg.storpool_template = None
          self.cfg.storpool_replication = 3
 +        self.cfg.storpool_iscsi_cinder_volume = False
 +        self.cfg.storpool_iscsi_export_to = ''
 +        self.cfg.storpool_iscsi_learn_initiator_iqns = True
-+        self.cfg.storpool_iscsi_portal_group = _ISCSI_PORTAL_GROUP
- 
-+        self._setup_test_driver()
++        self.cfg.storpool_iscsi_portal_group = ISCSI_PORTAL_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 = ('', '')
  
-@@ -246,7 +585,7 @@ class StorPoolTestCase(test.TestCase):
-         self.driver.check_for_setup_error()
+@@ -216,7 +473,7 @@ class StorPoolTestCase(test.TestCase):
+             self.driver.check_for_setup_error()
  
      @ddt.data(
 -        (5, TypeError),
@@ -435,7 +349,7 @@
          ({'no-host': None}, KeyError),
          ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
          ({'host': 's01'}, None),
-@@ -262,7 +601,7 @@ class StorPoolTestCase(test.TestCase):
+@@ -232,7 +489,7 @@ class StorPoolTestCase(test.TestCase):
                                conn)
  
      @ddt.data(
@@ -444,7 +358,7 @@
          ({'no-host': None}, KeyError),
          ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
      )
-@@ -301,7 +640,7 @@ class StorPoolTestCase(test.TestCase):
+@@ -271,7 +528,7 @@ class StorPoolTestCase(test.TestCase):
              self.assertEqual(21, pool['total_capacity_gb'])
              self.assertEqual(5, int(pool['free_capacity_gb']))
  
@@ -453,27 +367,27 @@
              self.assertFalse(pool['QoS_support'])
              self.assertFalse(pool['thick_provisioning_support'])
              self.assertTrue(pool['thin_provisioning_support'])
-@@ -720,3 +1059,179 @@ class StorPoolTestCase(test.TestCase):
+@@ -690,3 +947,179 @@ class StorPoolTestCase(test.TestCase):
                                 'No such volume',
                                 self.driver.revert_to_snapshot, None,
                                 {'id': vol_id}, {'id': snap_id})
 +
 +    @ddt.data(
 +        # The default values
-+        ('', False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
++        ('', False, constants.STORPOOL, ISCSI_IQN_OURS, False),
 +
 +        # Export to all
-+        ('*', True, constants.ISCSI, _ISCSI_IQN_OURS, True),
-+        ('*', True, constants.ISCSI, _ISCSI_IQN_OURS, True),
++        ('*', True, constants.ISCSI, ISCSI_IQN_OURS, True),
++        ('*', True, constants.ISCSI, ISCSI_IQN_OURS, True),
 +
 +        # Only export to the controller
-+        ('', False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
++        ('', False, constants.STORPOOL, ISCSI_IQN_OURS, False),
 +
 +        # Some of the not-fully-supported pattern lists
-+        (_ISCSI_PAT_OTHER, False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
-+        (_ISCSI_PAT_OTHER, False, constants.STORPOOL, _ISCSI_IQN_OTHER, True),
-+        (_ISCSI_PAT_BOTH, False, constants.STORPOOL, _ISCSI_IQN_OURS, True),
-+        (_ISCSI_PAT_BOTH, False, constants.STORPOOL, _ISCSI_IQN_OTHER, True),
++        (ISCSI_PAT_OTHER, False, constants.STORPOOL, ISCSI_IQN_OURS, False),
++        (ISCSI_PAT_OTHER, False, constants.STORPOOL, ISCSI_IQN_OTHER, True),
++        (ISCSI_PAT_BOTH, False, constants.STORPOOL, ISCSI_IQN_OURS, True),
++        (ISCSI_PAT_BOTH, False, constants.STORPOOL, ISCSI_IQN_OTHER, True),
 +    )
 +    @ddt.unpack
 +    def test_wants_iscsi(self, storpool_iscsi_export_to, use_iscsi,
@@ -492,7 +406,7 @@
 +        def check(conn, forced, expected):
 +            """Pass partially or completely valid connector info."""
 +            for initiator in (None, hostname):
-+                for host in (None, _ISCSI_IQN_THIRD):
++                for host in (None, ISCSI_IQN_THIRD):
 +                    self.assertEqual(
 +                        self.driver._connector_wants_iscsi({
 +                            "host": host,
@@ -513,16 +427,16 @@
 +
 +    def _validate_iscsi_config(
 +        self,
-+        cfg: MockIscsiConfig,
++        cfg: dict,
 +        res: dict[str, Any],
 +        tcase: IscsiTestCase,
 +    ) -> None:
 +        """Make sure the returned structure makes sense."""
 +        initiator = res['initiator']
-+        cfg_initiator = cfg.initiators.get('1')
++        cfg_initiator = cfg['initiators'].get('1')
 +
-+        self.assertIs(res['cfg'].iscsi, cfg)
-+        self.assertEqual(res['pg'].name, _ISCSI_PORTAL_GROUP)
++        self.assertIs(res['cfg']['iscsi'], cfg)
++        self.assertEqual(res['pg']['name'], ISCSI_PORTAL_GROUP)
 +
 +        if tcase.initiator is None:
 +            self.assertIsNone(initiator)
@@ -534,7 +448,7 @@
 +            self.assertIsNone(res['target'])
 +        else:
 +            self.assertIsNotNone(res['target'])
-+        self.assertEqual(res['target'], cfg.targets.get('1'))
++        self.assertEqual(res['target'], cfg['targets'].get('1'))
 +
 +        if tcase.initiator is None:
 +            self.assertIsNone(cfg_initiator)
@@ -543,7 +457,7 @@
 +            self.assertIsNotNone(cfg_initiator)
 +            if tcase.exported:
 +                self.assertIsNotNone(res['export'])
-+                self.assertEqual(res['export'], cfg_initiator.exports[0])
++                self.assertEqual(res['export'], cfg_initiator['exports'][0])
 +            else:
 +                self.assertIsNone(res['export'])
 +
@@ -553,9 +467,9 @@
 +        cfg_orig = MockIscsiConfig.build(tcase)
 +        configs = [cfg_orig]
 +        iapi = MockIscsiAPI(configs, self)
-+        with mock.patch.object(self.driver._attach, 'api', new=lambda: iapi):
++        with mock.patch.object(self.driver, '_sp_api', iapi):
 +            res = self.driver._get_iscsi_config(
-+                _ISCSI_IQN_OURS,
++                ISCSI_IQN_OURS,
 +                fake_constants.VOLUME_ID,
 +            )
 +
@@ -567,7 +481,7 @@
 +        cfg_orig = MockIscsiConfig.build(tcase)
 +        configs = [cfg_orig]
 +        iapi = MockIscsiAPI(configs, self)
-+        with mock.patch.object(self.driver._attach, 'api', new=lambda: iapi):
++        with mock.patch.object(self.driver, '_sp_api', iapi):
 +            self.driver._create_iscsi_export(
 +                {
 +                    'id': fake_constants.VOLUME_ID,
@@ -575,20 +489,20 @@
 +                },
 +                {
 +                    # Yeah, okay, so we cheat a little bit here...
-+                    'host': _ISCSI_IQN_OURS + '.hostname',
-+                    'initiator': _ISCSI_IQN_OURS,
++                    'host': ISCSI_IQN_OURS + '.hostname',
++                    'initiator': ISCSI_IQN_OURS,
 +                },
 +            )
 +
 +        self.assertEqual(len(configs), tcase.commands_count + 1)
 +        cfg_final = configs[-1]
-+        self.assertEqual(cfg_final.initiators['1'].name, _ISCSI_IQN_OURS)
++        self.assertEqual(cfg_final['initiators']['1']['name'], ISCSI_IQN_OURS)
 +        self.assertEqual(
-+            cfg_final.initiators['1'].exports[0].target,
++            cfg_final['initiators']['1']['exports'][0]['target'],
 +            targetName(fake_constants.VOLUME_ID),
 +        )
 +        self.assertEqual(
-+            cfg_final.targets['1'].volume,
++            cfg_final['targets']['1']['volume'],
 +            volumeName(fake_constants.VOLUME_ID),
 +        )
 +
@@ -598,43 +512,43 @@
 +        configs = [cfg_orig]
 +        iapi = MockIscsiAPI(configs, self)
 +
-+        def _target_exists(cfg: MockIscsiConfig, volume: str) -> bool:
-+            for name, target in cfg.targets.items():
-+                if target.volume == volumeName(volume):
++        def _target_exists(cfg: dict, volume: str) -> bool:
++            for name, target in cfg['targets'].items():
++                if target['volume'] == volumeName(volume):
 +                    return True
 +            return False
 +
-+        def _export_exists(cfg: MockIscsiConfig, volume: str) -> bool:
-+            for name, initiator in cfg.initiators.items():
-+                for export in initiator.exports:
-+                    if export.target == targetName(volume):
++        def _export_exists(cfg: dict, volume: str) -> bool:
++            for name, initiator in cfg['initiators'].items():
++                for export in initiator['exports']:
++                    if export['target'] == targetName(volume):
 +                        return True
 +            return False
 +
 +        if tcase.exported:
 +            self.assertTrue(
-+                _target_exists(iapi.iSCSIConfig().iscsi, tcase.volume))
++                _target_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
 +            self.assertTrue(
-+                _export_exists(iapi.iSCSIConfig().iscsi, tcase.volume))
++                _export_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
 +
-+        with mock.patch.object(self.driver._attach, 'api', new=lambda: iapi):
++        with mock.patch.object(self.driver, '_sp_api', iapi):
 +            self.driver._remove_iscsi_export(
 +                {
 +                    'id': fake_constants.VOLUME_ID,
 +                    'display_name': fake_constants.VOLUME_NAME,
 +                },
 +                {
-+                    'host': _ISCSI_IQN_OURS + '.hostname',
-+                    'initiator': _ISCSI_IQN_OURS,
++                    'host': ISCSI_IQN_OURS + '.hostname',
++                    'initiator': ISCSI_IQN_OURS,
 +                },
 +            )
 +
 +        self.assertFalse(
-+            _target_exists(iapi.iSCSIConfig().iscsi, tcase.volume))
++            _target_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
 +        self.assertFalse(
-+            _export_exists(iapi.iSCSIConfig().iscsi, tcase.volume))
++            _export_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
 diff --git a/cinder/volume/drivers/storpool.py b/cinder/volume/drivers/storpool.py
-index a8200a7f1..d931200f6 100644
+index 2dc7bb6be..d9e607552 100644
 --- a/cinder/volume/drivers/storpool.py
 +++ b/cinder/volume/drivers/storpool.py
 @@ -15,6 +15,7 @@
@@ -644,8 +558,8 @@
 +import fnmatch
  import platform
  
- from oslo_config import cfg
-@@ -43,6 +44,32 @@ if storpool:
+ from os_brick.initiator import storpool_utils
+@@ -36,6 +37,32 @@ LOG = logging.getLogger(__name__)
  
  
  storpool_opts = [
@@ -678,27 +592,28 @@
      cfg.StrOpt('storpool_template',
                 default=None,
                 help='The StorPool template for volumes with no type.'),
-@@ -93,9 +120,10 @@ class StorPoolDriver(driver.VolumeDriver):
-                   add ignore_errors to the internal _detach_volume() method
-         1.2.3   - Advertise some more driver capabilities.
-         2.0.0   - Implement revert_to_snapshot().
-+        2.1.0   - Add iSCSI export support.
+@@ -89,10 +116,10 @@ class StorPoolDriver(driver.VolumeDriver):
+         2.1.0   - Use the new API client in os-brick to communicate with the
+                   StorPool API instead of packages `storpool` and
+                   `storpool.spopenstack`
+-
++        2.2.0   - Add iSCSI export support.
      """
  
--    VERSION = '2.0.0'
-+    VERSION = '2.1.0'
+-    VERSION = '2.1.0'
++    VERSION = '2.2.0'
      CI_WIKI_NAME = 'StorPool_distributed_storage_CI'
  
      def __init__(self, *args, **kwargs):
-@@ -105,6 +133,7 @@ class StorPoolDriver(driver.VolumeDriver):
-         self._ourId = None
+@@ -103,6 +130,7 @@ class StorPoolDriver(driver.VolumeDriver):
          self._ourIdInt = None
-         self._attach = None
+         self._sp_api = None
+         self._volume_prefix = None
 +        self._use_iscsi = False
  
      @staticmethod
      def get_driver_options():
-@@ -159,10 +188,327 @@ class StorPoolDriver(driver.VolumeDriver):
+@@ -158,10 +186,322 @@ class StorPoolDriver(driver.VolumeDriver):
              raise StorPoolConfigurationInvalid(
                  section=hostname, param='SP_OURID', error=e)
  
@@ -714,25 +629,20 @@
 +        if connector is None:
 +            return False
 +        if self._use_iscsi:
-+            LOG.debug('  - forcing iSCSI for all exported volumes')
++            LOG.debug('forcing iSCSI for all exported volumes')
 +            return True
 +        if connector.get('storpool_wants_iscsi'):
-+            LOG.debug('  - forcing iSCSI for the controller')
++            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
++        iqn = connector.get('initiator', None)
++        host = connector.get('host', None)
++
 +        if iqn is None or host is None:
-+            LOG.debug('  - this connector certainly does not want iSCSI')
++            LOG.debug('this connector certainly does not want iSCSI')
 +            return False
 +
-+        LOG.debug('  - check whether %(host)s (%(iqn)s) wants iSCSI',
++        LOG.debug('check whether %(host)s (%(iqn)s) wants iSCSI',
 +                  {
 +                      'host': host,
 +                      'iqn': iqn,
@@ -743,11 +653,11 @@
 +            return False
 +
 +        for pat in export_to.split():
-+            LOG.debug('    - matching against %(pat)s', {'pat': pat})
++            LOG.debug('matching against %(pat)s', {'pat': pat})
 +            if fnmatch.fnmatch(iqn, pat):
-+                LOG.debug('      - got it!')
++                LOG.debug('got it!')
 +                return True
-+        LOG.debug('    - nope')
++        LOG.debug('nope')
 +        return False
 +
      def validate_connector(self, connector):
@@ -762,11 +672,11 @@
 +        will be needed to create, ensure, or remove the iSCSI export of
 +        the specified volume to the specified initiator.
 +        """
-+        cfg = self._attach.api().iSCSIConfig()
++        cfg = self._sp_api.get_iscsi_config()
 +
 +        pg_name = self.configuration.storpool_iscsi_portal_group
 +        pg_found = [
-+            pg for pg in cfg.iscsi.portalGroups.values() if pg.name == pg_name
++            pg for pg in cfg['iscsi']['portalGroups'].values() if pg['name'] == pg_name
 +        ]
 +        if not pg_found:
 +            raise Exception('StorPool Cinder iSCSI configuration error: '
@@ -775,7 +685,7 @@
 +
 +        # Do we know about this initiator?
 +        i_found = [
-+            init for init in cfg.iscsi.initiators.values() if init.name == iqn
++            init for init in cfg['iscsi']['initiators'].values() if init['name'] == iqn
 +        ]
 +        if i_found:
 +            initiator = i_found[0]
@@ -783,9 +693,9 @@
 +            initiator = None
 +
 +        # Is this volume already being exported?
-+        volname = self._attach.volumeName(volume_id)
++        volname = storpool_utils.os_to_sp_volume_name(volume_id)
 +        t_found = [
-+            tgt for tgt in cfg.iscsi.targets.values() if tgt.volume == volname
++            tgt for tgt in cfg['iscsi']['targets'].values() if tgt['volume'] == volname
 +        ]
 +        if t_found:
 +            target = t_found[0]
@@ -796,8 +706,8 @@
 +        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
++                exp for exp in initiator['exports']
++                if exp['portalGroup'] == pg['name'] and exp['target'] == target['name']
 +            ]
 +            if e_found:
 +                export = e_found[0]
@@ -844,7 +754,7 @@
 +                LOG.info('Creating a StorPool iSCSI initiator '
 +                         'for "{host}s" ({iqn}s)',
 +                         {'host': connector['host'], 'iqn': iqn})
-+                self._attach.api().iSCSIConfigChange({
++                self._sp_api.post_iscsi_config({
 +                    'commands': [
 +                        {
 +                            'createInitiator': {
@@ -871,7 +781,7 @@
 +                    'vol_id': volume['id'],
 +                }
 +            )
-+            self._attach.api().iSCSIConfigChange({
++            self._sp_api.post_iscsi_config({
 +                'commands': [
 +                    {
 +                        'createTarget': {
@@ -892,14 +802,14 @@
 +                         'vol_id': volume['id'],
 +                         'host': connector['host'],
 +                         'iqn': iqn,
-+                         'pg': cfg['pg'].name
++                         'pg': cfg['pg']['name']
 +                     })
-+            self._attach.api().iSCSIConfigChange({
++            self._sp_api.post_iscsi_config({
 +                'commands': [
 +                    {
 +                        'export': {
 +                            'initiator': iqn,
-+                            'portalGroup': cfg['pg'].name,
++                            'portalGroup': cfg['pg']['name'],
 +                            'volumeName': cfg['volume_name'],
 +                        },
 +                    },
@@ -907,10 +817,10 @@
 +            })
 +
 +        target_portals = [
-+            "{addr}:3260".format(addr=net.address)
-+            for net in cfg['pg'].networks
++            "{addr}:3260".format(addr=net['address'])
++            for net in cfg['pg']['networks']
 +        ]
-+        target_iqns = [cfg['target'].name] * len(target_portals)
++        target_iqns = [cfg['target']['name']] * len(target_portals)
 +        target_luns = [0] * len(target_portals)
 +        if connector.get('multipath', False):
 +            multipath_settings = {
@@ -965,32 +875,32 @@
 +                         'vol_id': volume['id'],
 +                         'host': connector['host'],
 +                         'iqn': connector['initiator'],
-+                         'pg': cfg['pg'].name,
++                         'pg': cfg['pg']['name'],
 +                     })
 +            try:
-+                self._attach.api().iSCSIConfigChange({
++                self._sp_api.post_iscsi_config({
 +                    'commands': [
 +                        {
 +                            'exportDelete': {
-+                                'initiator': cfg['initiator'].name,
-+                                'portalGroup': cfg['pg'].name,
++                                'initiator': cfg['initiator']['name'],
++                                'portalGroup': cfg['pg']['name'],
 +                                'volumeName': cfg['volume_name'],
 +                            },
 +                        },
 +                    ]
 +                })
-+            except spapi.ApiError as e:
++            except storpool_utils.StorPoolAPIError 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:
++            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:
++                for exp in initiator['exports']:
++                    if exp['target'] == cfg['target']['name']:
 +                        last = False
 +                        break
 +                if not last:
@@ -1006,7 +916,7 @@
 +                    }
 +                )
 +                try:
-+                    self._attach.api().iSCSIConfigChange({
++                    self._sp_api.post_iscsi_config({
 +                        'commands': [
 +                            {
 +                                'deleteTarget': {
@@ -1015,7 +925,7 @@
 +                            },
 +                        ]
 +                    })
-+                except spapi.ApiError as e:
++                except storpool_utils.StorPoolAPIError as e:
 +                    if e.name not in ('objectDoesNotExist', 'invalidParam'):
 +                        raise
 +                    LOG.info('Looks like somebody beat us to it')
@@ -1026,23 +936,23 @@
          return {'driver_volume_type': 'storpool',
                  'data': {
                      'client_id': self._storpool_client_id(connector),
-@@ -171,6 +517,9 @@ class StorPoolDriver(driver.VolumeDriver):
+@@ -170,6 +510,9 @@ class StorPoolDriver(driver.VolumeDriver):
                  }}
  
      def terminate_connection(self, volume, connector, **kwargs):
 +        if self._connector_wants_iscsi(connector):
-+            LOG.debug('- removing an iSCSI export')
++            LOG.debug('removing an iSCSI export')
 +            self._remove_iscsi_export(volume, connector)
          pass
  
      def create_snapshot(self, snapshot):
-@@ -272,11 +621,20 @@ class StorPoolDriver(driver.VolumeDriver):
+@@ -278,11 +621,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')
++            LOG.debug('creating an iSCSI export')
 +            self._create_iscsi_export(volume, connector)
  
      def remove_export(self, context, volume):
@@ -1050,15 +960,15 @@
  
 +    def _attach_volume(self, context, volume, properties, remote=False):
 +        if self.configuration.storpool_iscsi_cinder_volume and not remote:
-+            LOG.debug('- adding the "storpool_wants_iscsi" flag')
++            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:
-@@ -313,6 +671,17 @@ class StorPoolDriver(driver.VolumeDriver):
+         name = storpool_utils.os_to_sp_volume_name(
+             self._volume_prefix, volume['id'])
+@@ -321,6 +673,17 @@ class StorPoolDriver(driver.VolumeDriver):
              LOG.error("StorPoolDriver API initialization failed: %s", e)
              raise
  
@@ -1075,8 +985,8 @@
 +
      def _update_volume_stats(self):
          try:
-             dl = self._attach.api().disksList()
-@@ -338,7 +707,7 @@ class StorPoolDriver(driver.VolumeDriver):
+             dl = self._sp_api.disks_list()
+@@ -346,7 +709,7 @@ class StorPoolDriver(driver.VolumeDriver):
              'total_capacity_gb': total / units.Gi,
              'free_capacity_gb': free / units.Gi,
              'reserved_percentage': 0,
@@ -1085,7 +995,7 @@
              'QoS_support': False,
              'thick_provisioning_support': False,
              'thin_provisioning_support': True,
-@@ -357,7 +726,9 @@ class StorPoolDriver(driver.VolumeDriver):
+@@ -365,7 +728,9 @@ class StorPoolDriver(driver.VolumeDriver):
                  'volume_backend_name') or 'storpool',
              'vendor_name': 'StorPool',
              'driver_version': self.VERSION,
@@ -1096,6 +1006,25 @@
              # Driver capabilities
              'clone_across_pools': True,
              'sparse_copy_volume': True,
+@@ -452,15 +817,15 @@ class StorPoolDriver(driver.VolumeDriver):
+             LOG.debug('Trying to swap volume names, intermediate "%(int)s"',
+                       {'int': int_name})
+             try:
+-                LOG.debug('- rename "%(orig)s" to "%(int)s"',
++                LOG.debug('rename "%(orig)s" to "%(int)s"',
+                           {'orig': orig_name, 'int': int_name})
+                 self._sp_api.volume_update(orig_name, {'rename': int_name})
+ 
+-                LOG.debug('- rename "%(temp)s" to "%(orig)s"',
++                LOG.debug('rename "%(temp)s" to "%(orig)s"',
+                           {'temp': temp_name, 'orig': orig_name})
+                 self._sp_api.volume_update(temp_name, {'rename': orig_name})
+ 
+-                LOG.debug('- rename "%(int)s" to "%(temp)s"',
++                LOG.debug('rename "%(int)s" to "%(temp)s"',
+                           {'int': int_name, 'temp': temp_name})
+                 self._sp_api.volume_update(int_name, {'rename': temp_name})
+                 return {'_name_id': None}
 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..936e83675 100644
 --- a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst