Temporarily renegenerate the iSCSI patch
Change-Id: I36e5e115a164c69e8637d5848b4970c8b94ab856
diff --git a/patches/openstack/cinder/sep-sp-iscsi.patch b/patches/openstack/cinder/sep-sp-iscsi.patch
index 1ec0908..3b7ed42 100644
--- a/patches/openstack/cinder/sep-sp-iscsi.patch
+++ b/patches/openstack/cinder/sep-sp-iscsi.patch
@@ -3,6 +3,8 @@
Date: Mon, 12 Mar 2018 12:00:10 +0200
Subject: [PATCH] Add iSCSI export support to the StorPool driver
+Add iSCSI export support to the StorPool driver
+
Add four new driver options:
- storpool_iscsi_cinder_volume: use StorPool iSCSI attachments whenever
the cinder-volume service needs to attach a volume to the controller,
@@ -25,53 +27,36 @@
Change-Id: I9de64306e0e6976268df782053b0651dd1cca96f
---
- .../unit/volume/drivers/test_storpool.py | 521 +++++++++++++++++-
- cinder/volume/drivers/storpool.py | 379 ++++++++++++-
- .../drivers/storpool-volume-driver.rst | 68 ++-
- .../storpool-iscsi-cefcfe590a07c5c7.yaml | 15 +
- 4 files changed, 972 insertions(+), 11 deletions(-)
- create mode 100644 releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
+ .../unit/volume/drivers/test_storpool.py | 438 +++++++++++++++++-
+ cinder/volume/drivers/storpool.py | 410 +++++++++++++++-
+ .../drivers/storpool-volume-driver.rst | 64 ++-
+ 3 files changed, 901 insertions(+), 11 deletions(-)
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 cee3fdd67..3dd84ec4a 100644
--- a/cinder/tests/unit/volume/drivers/test_storpool.py
+++ b/cinder/tests/unit/volume/drivers/test_storpool.py
-@@ -14,14 +14,24 @@
- # under the License.
+@@ -15,14 +15,17 @@
-+from __future__ import annotations
-+
+ import copy
+import dataclasses
import itertools
+ import os
import re
- import sys
+from typing import Any, NamedTuple, TYPE_CHECKING # noqa: H301
from unittest import mock
import ddt
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
- from cinder.volume.drivers import storpool as driver
-
+@@ -56,6 +59,12 @@ SP_CONF = {
+ 'SP_OURID': '1'
+ }
+_ISCSI_IQN_OURS = 'beleriand'
+_ISCSI_IQN_OTHER = 'rohan'
@@ -79,64 +64,24 @@
+_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)
+@@ -94,6 +103,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 = {}
+@@ -204,6 +217,241 @@ class MockVolumeDB(object):
+ 'volume_type': self.vol_types.get(vid),
+ }
-
-+@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,92 +95,81 @@
+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,
@@ -245,30 +179,24 @@
+ '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,
@@ -278,21 +206,16 @@
+ '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,
@@ -304,61 +227,49 @@
+ )
+ 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,
@@ -367,7 +278,7 @@
+ 'net': '0.0.0.0/0',
+ },
+ )
-+ return dataclasses.replace(cfg)
++ return cfg
+
+ _CMD_HANDLERS = {
+ 'createInitiator': _handle_create_initiator,
@@ -378,7 +289,7 @@
+ 'initiatorAddNetwork': _handle_initiator_add_network,
+ }
+
-+ def iSCSIConfigChange(
++ def post_iscsi_config(
+ self,
+ commands: dict[str, list[dict[str, dict[str, Any]]]],
+ ) -> None:
@@ -406,11 +317,10 @@
+ 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):
+@@ -383,7 +631,15 @@ class StorPoolTestCase(test.TestCase):
self.cfg.volume_backend_name = 'storpool_test'
self.cfg.storpool_template = None
self.cfg.storpool_replication = 3
@@ -426,8 +336,8 @@
mock_exec = mock.Mock()
mock_exec.return_value = ('', '')
-@@ -246,7 +585,7 @@ class StorPoolTestCase(test.TestCase):
- self.driver.check_for_setup_error()
+@@ -400,7 +656,7 @@ class StorPoolTestCase(test.TestCase):
+ self.driver.check_for_setup_error()
@ddt.data(
- (5, TypeError),
@@ -435,7 +345,7 @@
({'no-host': None}, KeyError),
({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
({'host': 's01'}, None),
-@@ -262,7 +601,7 @@ class StorPoolTestCase(test.TestCase):
+@@ -416,7 +672,7 @@ class StorPoolTestCase(test.TestCase):
conn)
@ddt.data(
@@ -444,7 +354,7 @@
({'no-host': None}, KeyError),
({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
)
-@@ -301,7 +640,7 @@ class StorPoolTestCase(test.TestCase):
+@@ -455,7 +711,7 @@ class StorPoolTestCase(test.TestCase):
self.assertEqual(21, pool['total_capacity_gb'])
self.assertEqual(5, int(pool['free_capacity_gb']))
@@ -453,7 +363,7 @@
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):
+@@ -874,3 +1130,179 @@ class StorPoolTestCase(test.TestCase):
'No such volume',
self.driver.revert_to_snapshot, None,
{'id': vol_id}, {'id': snap_id})
@@ -513,16 +423,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 +444,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 +453,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,7 +463,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):
+ res = self.driver._get_iscsi_config(
+ _ISCSI_IQN_OURS,
+ fake_constants.VOLUME_ID,
@@ -567,7 +477,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,
@@ -582,13 +492,13 @@
+
+ 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,26 +508,26 @@
+ 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,
@@ -630,22 +540,33 @@
+ )
+
+ 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 1707427fa..6a6f5b2e5 100644
--- a/cinder/volume/drivers/storpool.py
+++ b/cinder/volume/drivers/storpool.py
-@@ -15,6 +15,7 @@
+@@ -17,6 +17,7 @@
- """StorPool block device driver"""
-
+ import configparser
+ import errno
+import fnmatch
- import platform
-
+ import http.client
+ import json
+ import os
+@@ -28,7 +29,10 @@ import time
from oslo_config import cfg
-@@ -43,6 +44,32 @@ if storpool:
+ from oslo_log import log as logging
+ from oslo_utils import excutils
++from oslo_utils import netutils
+ from oslo_utils import units
++from oslo_utils import uuidutils
++import six
+
+ from cinder.common import constants
+ from cinder import context
+@@ -46,6 +50,32 @@ DEV_STORPOOL_BYID = pathlib.Path('/dev/storpool-byid')
storpool_opts = [
@@ -678,27 +599,69 @@
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.
+@@ -61,6 +91,28 @@ CONF = cfg.CONF
+ CONF.register_opts(storpool_opts, group=configuration.SHARED_CONF_GROUP)
+
+
++def _extract_cinder_ids(urls):
++ ids = []
++ for url in urls:
++ # The url can also be None and a TypeError is raised
++ # TypeError: a bytes-like object is required, not 'str'
++ if not url:
++ continue
++ parts = netutils.urlsplit(url)
++ if parts.scheme == 'cinder':
++ if parts.path:
++ vol_id = parts.path.split('/')[-1]
++ else:
++ vol_id = parts.netloc
++ if uuidutils.is_uuid_like(vol_id):
++ ids.append(vol_id)
++ else:
++ LOG.debug("Ignoring malformed image location uri "
++ "'%(url)s'", {'url': url})
++
++ return ids
++
++
+ class StorPoolConfigurationInvalid(exception.CinderException):
+ message = _("Invalid parameter %(param)s in the %(section)s section "
+ "of the /etc/storpool.conf file: %(error)s")
+@@ -233,6 +285,12 @@ class StorPoolAPI:
+ self._api_call(
+ 'POST', f'/ctrl/1.0/MultiCluster/SnapshotDelete/{snapshot}')
+
++ def get_iscsi_config(self):
++ return self._api_call('GET', '/ctrl/1.0/iSCSIConfig')
++
++ def post_iscsi_config(self, data):
++ return self._api_call('POST', '/ctrl/1.0/iSCSIConfig', data)
++
+
+ @interface.volumedriver
+ class StorPoolDriver(driver.VolumeDriver):
+@@ -267,9 +325,10 @@ class StorPoolDriver(driver.VolumeDriver):
+ 2.1.0 - Use a new in-tree API client 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
+@@ -280,6 +339,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):
+@@ -351,10 +411,327 @@ class StorPoolDriver(driver.VolumeDriver):
raise StorPoolConfigurationInvalid(
section=hostname, param='SP_OURID', error=e)
@@ -762,11 +725,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 +738,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 +746,9 @@
+ initiator = None
+
+ # Is this volume already being exported?
-+ volname = self._attach.volumeName(volume_id)
++ volname = self._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 +759,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 +807,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 +834,7 @@
+ 'vol_id': volume['id'],
+ }
+ )
-+ self._attach.api().iSCSIConfigChange({
++ self._sp_api.post_iscsi_config({
+ 'commands': [
+ {
+ 'createTarget': {
@@ -892,14 +855,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 +870,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 +928,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 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 +969,7 @@
+ }
+ )
+ try:
-+ self._attach.api().iSCSIConfigChange({
++ self._sp_api.post_iscsi_config({
+ 'commands': [
+ {
+ 'deleteTarget': {
@@ -1015,7 +978,7 @@
+ },
+ ]
+ })
-+ except spapi.ApiError as e:
++ except StorPoolAPIError as e:
+ if e.name not in ('objectDoesNotExist', 'invalidParam'):
+ raise
+ LOG.info('Looks like somebody beat us to it')
@@ -1026,7 +989,7 @@
return {'driver_volume_type': 'storpool',
'data': {
'client_id': self._storpool_client_id(connector),
-@@ -171,6 +517,9 @@ class StorPoolDriver(driver.VolumeDriver):
+@@ -363,6 +740,9 @@ class StorPoolDriver(driver.VolumeDriver):
}}
def terminate_connection(self, volume, connector, **kwargs):
@@ -1036,7 +999,7 @@
pass
def create_snapshot(self, snapshot):
-@@ -272,11 +621,20 @@ class StorPoolDriver(driver.VolumeDriver):
+@@ -464,11 +844,20 @@ class StorPoolDriver(driver.VolumeDriver):
)
def create_export(self, context, volume, connector):
@@ -1056,9 +1019,9 @@
+ return super()._attach_volume(context, volume, properties, remote)
+
def delete_volume(self, volume):
- name = self._attach.volumeName(volume['id'])
+ name = self._os_to_sp_volume_name(volume['id'])
try:
-@@ -313,6 +671,17 @@ class StorPoolDriver(driver.VolumeDriver):
+@@ -502,6 +891,17 @@ class StorPoolDriver(driver.VolumeDriver):
LOG.error("StorPoolDriver API initialization failed: %s", e)
raise
@@ -1075,8 +1038,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()
+@@ -527,7 +927,7 @@ class StorPoolDriver(driver.VolumeDriver):
'total_capacity_gb': total / units.Gi,
'free_capacity_gb': free / units.Gi,
'reserved_percentage': 0,
@@ -1085,7 +1048,7 @@
'QoS_support': False,
'thick_provisioning_support': False,
'thin_provisioning_support': True,
-@@ -357,7 +726,9 @@ class StorPoolDriver(driver.VolumeDriver):
+@@ -546,7 +946,9 @@ class StorPoolDriver(driver.VolumeDriver):
'volume_backend_name') or 'storpool',
'vendor_name': 'StorPool',
'driver_version': self.VERSION,
@@ -1097,7 +1060,7 @@
'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..936e83675 100644
+index d2c5895a9..1ba0d2862 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
@@ -1120,7 +1083,7 @@
* 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,34 @@ Prerequisites
+@@ -34,6 +37,33 @@ Prerequisites
* the storpool Python bindings package
* the storpool.spopenstack Python helper package
@@ -1137,25 +1100,24 @@
+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 ``storpool_iscsi_export_to`` and
-+``storpool_iscsi_portal_group`` configuration options.
++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 ``storpool_iscsi_export_to`` option has its default empty string
-+value), the ``storpool_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.
++(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.
+
+Multiattach support for StorPool is only enabled if iSCSI is used:
-+``storpool_iscsi_export_to`` is set to ``*``, that is, when all StorPool
-+volumes will be exported via iSCSI to all initiators.
++``iscsi_export_to`` is set to ``*``, that is, when all StorPool volumes
++will be exported via iSCSI to all initiators.
+
Configuring the StorPool volume driver
--------------------------------------
-@@ -55,6 +86,35 @@ volume backend definition) and per volume type:
+@@ -55,6 +85,32 @@ 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.
@@ -1163,55 +1125,31 @@
+described in the previous section, the following options may be defined in
+the ``cinder.conf`` volume backend definition:
+
-+- ``storpool_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
-+ ``storpool_iscsi_portal_group`` option must also be specified.
++- ``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.
+
-+- ``storpool_iscsi_portal_group``: if the ``storpool_iscsi_export_to``
-+ option is set to the value ``*`` or the
-+ ``storpool_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_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.
+
-+- ``storpool_iscsi_cinder_volume``: if enabled, even if the
-+ ``storpool_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 if Glance is
-+ configured to use the Cinder glance_store.
++- ``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 if Glance is configured to use the Cinder glance_store.
+
-+- ``storpool_iscsi_learn_initiator_iqns``: if enabled, the StorPool
-+ Cinder driver will automatically use the StorPool API to create
-+ definitions for new initiators in the StorPool cluster's
-+ configuration. This is the default behavior of the driver; it may be
-+ disabled in the rare case if, e.g. because of site policy, OpenStack
-+ iSCSI initiators (e.g. Nova hypervisors) need to be explicitly allowed
-+ to use the StorPool iSCSI targets.
++- ``iscsi_learn_initiator_iqns``: if enabled, the StorPool Cinder driver will
++ automatically use the StorPool API to create definitions for new initiators
++ in the StorPool cluster's configuration. This is the default behavior of
++ the driver; it may be disabled in the rare case if, e.g. because of site
++ policy, OpenStack iSCSI initiators (e.g. Nova hypervisors) need to be
++ explicitly allowed to use the StorPool iSCSI targets.
+
Using the StorPool volume driver
--------------------------------
-diff --git a/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml b/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
-new file mode 100644
-index 000000000..3863e4099
---- /dev/null
-+++ b/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
-@@ -0,0 +1,15 @@
-+features:
-+ - |
-+ StorPool driver: Added support for exporting the StorPool-backed
-+ volumes using the iSCSI protocol, so that the Cinder volume service
-+ and/or the Nova or Glance consumers do not need to have the StorPool
-+ block device third-party service installed. See the StorPool driver
-+ section in the Cinder documentation for more information on the
-+ ``storpool_iscsi_export_to``, ``storpool_iscsi_portal_group``,
-+ ``storpool_iscsi_cinder_volume``, and
-+ ``storpool_iscsi_learn_initiator_iqns`` options.
-+
-+ .. note::
-+ Multiattach support for StorPool is now only enabled if
-+ ``storpool_iscsi_export_to`` is set to ``*``, that is, when all
-+ StorPool volumes will be exported via iSCSI to all initiators.
--
2.43.0