blob: 90af1b20e913ee2fcf11dad2ac13f46eabbad972 [file] [log] [blame]
Biser Milanovc6c42e02025-01-27 14:52:39 +02001From 98e9487ad99ac6969e5ae58601ab5550b947202b Mon Sep 17 00:00:00 2001
Peter Pentchev9c24be92022-09-26 22:35:24 +03002From: Peter Penchev <openstack-dev@storpool.com>
Biser Milanov8323a8c2024-11-20 10:53:27 +02003Date: Mon, 12 Mar 2018 12:00:10 +0200
Biser Milanovd684c1c2024-11-22 12:12:59 +02004Subject: [PATCH] Add iSCSI export support to the StorPool driver
Peter Pentchev9c24be92022-09-26 22:35:24 +03005
6Add four new driver options:
Biser Milanovd684c1c2024-11-22 12:12:59 +02007- storpool_iscsi_cinder_volume: use StorPool iSCSI attachments whenever
Peter Pentchev9c24be92022-09-26 22:35:24 +03008 the cinder-volume service needs to attach a volume to the controller,
9 e.g. for copying an image to a volume or vice versa
Biser Milanovd684c1c2024-11-22 12:12:59 +020010- storpool_iscsi_export_to:
11 - an empty string to use the StorPool native protocol for exporting
12 volumes
Peter Pentchev9c24be92022-09-26 22:35:24 +030013 - the string "*" to always use iSCSI for exporting volumes
14 - an experimental, not fully supported list of IQN patterns to export
15 volumes to using iSCSI; this results in a Cinder driver that exports
16 different volumes using different storage protocols
Biser Milanovd684c1c2024-11-22 12:12:59 +020017- storpool_iscsi_portal_group: the name of the iSCSI portal group
18 defined in the StorPool configuration to use for these export
19- storpool_iscsi_learn_initiator_iqns: automatically create StorPool
20 configuration records for an initiator when a volume is first exported
21 to it
Peter Pentchev9c24be92022-09-26 22:35:24 +030022
Biser Milanovd684c1c2024-11-22 12:12:59 +020023When exporting volumes via iSCSI, report the storage protocol as
24"iSCSI".
Peter Pentchev9c24be92022-09-26 22:35:24 +030025
26Change-Id: I9de64306e0e6976268df782053b0651dd1cca96f
27---
Biser Milanov233be152025-01-24 17:35:11 +020028 .../unit/volume/drivers/test_storpool.py | 441 +++++++++++++++++-
Biser Milanovc6c42e02025-01-27 14:52:39 +020029 cinder/volume/drivers/storpool.py | 386 ++++++++++++++-
Biser Milanovda1b0682024-11-29 09:37:10 +020030 .../drivers/storpool-volume-driver.rst | 68 ++-
31 .../storpool-iscsi-cefcfe590a07c5c7.yaml | 15 +
Biser Milanovc6c42e02025-01-27 14:52:39 +020032 4 files changed, 895 insertions(+), 15 deletions(-)
Biser Milanovda1b0682024-11-29 09:37:10 +020033 create mode 100644 releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
Biser Milanovc0ee3582025-01-22 09:09:24 +000034
Peter Pentchevacaaa382023-02-28 11:26:13 +020035diff --git a/cinder/tests/unit/volume/drivers/test_storpool.py b/cinder/tests/unit/volume/drivers/test_storpool.py
Biser Milanovc6c42e02025-01-27 14:52:39 +020036index 2015c734d..b21f1582d 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +030037--- a/cinder/tests/unit/volume/drivers/test_storpool.py
38+++ b/cinder/tests/unit/volume/drivers/test_storpool.py
Biser Milanovc6c42e02025-01-27 14:52:39 +020039@@ -13,9 +13,12 @@
Biser Milanov233be152025-01-24 17:35:11 +020040 # License for the specific language governing permissions and limitations
Biser Milanovda1b0682024-11-29 09:37:10 +020041 # under the License.
Peter Pentchevea354462023-07-18 11:15:56 +030042
Biser Milanovc6c42e02025-01-27 14:52:39 +020043+from __future__ import annotations
44
Peter Pentchevea354462023-07-18 11:15:56 +030045+import dataclasses
46 import itertools
47 import re
Biser Milanovc6c42e02025-01-27 14:52:39 +020048+from typing import Any, NamedTuple # noqa: H301
Peter Pentchevea354462023-07-18 11:15:56 +030049 from unittest import mock
50
51 import ddt
Biser Milanovc6c42e02025-01-27 14:52:39 +020052@@ -23,6 +26,7 @@ from os_brick.initiator import storpool_utils
Biser Milanov233be152025-01-24 17:35:11 +020053 from os_brick.tests.initiator import test_storpool_utils
Peter Pentchevea354462023-07-18 11:15:56 +030054 from oslo_utils import units
Peter Pentchevea354462023-07-18 11:15:56 +030055
Peter Pentchev9c24be92022-09-26 22:35:24 +030056+from cinder.common import constants
57 from cinder import exception
Biser Milanovec3bf982024-11-27 17:42:17 +020058 from cinder.tests.unit import fake_constants
Peter Pentchev9c24be92022-09-26 22:35:24 +030059 from cinder.tests.unit import test
Biser Milanovc6c42e02025-01-27 14:52:39 +020060@@ -30,6 +34,13 @@ from cinder.volume import configuration as conf
Biser Milanovda1b0682024-11-29 09:37:10 +020061 from cinder.volume.drivers import storpool as driver
62
Peter Pentchevea354462023-07-18 11:15:56 +030063
Biser Milanov233be152025-01-24 17:35:11 +020064+ISCSI_IQN_OURS = 'beleriand'
65+ISCSI_IQN_OTHER = 'rohan'
66+ISCSI_IQN_THIRD = 'gondor'
67+ISCSI_PAT_OTHER = 'roh*'
68+ISCSI_PAT_BOTH = '*riand roh*'
69+ISCSI_PORTAL_GROUP = 'openstack_pg'
Biser Milanovc0ee3582025-01-22 09:09:24 +000070+
71 volume_types = {
72 fake_constants.VOLUME_TYPE_ID: {},
73 fake_constants.VOLUME_TYPE2_ID: {'storpool_template': 'ssd'},
Biser Milanovc6c42e02025-01-27 14:52:39 +020074@@ -67,6 +78,10 @@ def snapshotName(vtype, vid, more=None):
Biser Milanov233be152025-01-24 17:35:11 +020075 )
Peter Pentchevea354462023-07-18 11:15:56 +030076
77
78+def targetName(vid):
79+ return 'iqn.2012-11.storpool:{id}'.format(id=vid)
80+
81+
Biser Milanov233be152025-01-24 17:35:11 +020082 class MockAPI(object):
83 def __init__(self, *args):
84 self._disks = {}
Biser Milanovc6c42e02025-01-27 14:52:39 +020085@@ -162,6 +177,242 @@ class MockAPI(object):
Biser Milanov233be152025-01-24 17:35:11 +020086 volumes[name] = dict(snapshots[snapname])
Peter Pentchevea354462023-07-18 11:15:56 +030087
Biser Milanovc0ee3582025-01-22 09:09:24 +000088
Peter Pentchevea354462023-07-18 11:15:56 +030089+class IscsiTestCase(NamedTuple):
90+ """A single test case for the iSCSI config and export methods."""
91+
92+ initiator: str | None
93+ volume: str | None
94+ exported: bool
95+ commands_count: int
96+
97+
98+@dataclasses.dataclass(frozen=True)
99+class MockIscsiConfig:
100+ """Mock the structure returned by the "get current config" query."""
101+
Peter Pentchevea354462023-07-18 11:15:56 +0300102+ @classmethod
Biser Milanov233be152025-01-24 17:35:11 +0200103+ def build(cls, tcase: IscsiTestCase) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300104+ """Build a test config structure."""
105+ initiators = {
Biser Milanov233be152025-01-24 17:35:11 +0200106+ '0': {'name': ISCSI_IQN_OTHER, 'exports': []},
Peter Pentchevea354462023-07-18 11:15:56 +0300107+ }
108+ if tcase.initiator is not None:
Biser Milanov233be152025-01-24 17:35:11 +0200109+ initiators['1'] = {
110+ 'name': tcase.initiator,
111+ 'exports': (
Peter Pentchevea354462023-07-18 11:15:56 +0300112+ [
Biser Milanov233be152025-01-24 17:35:11 +0200113+ {
114+ 'portalGroup': ISCSI_PORTAL_GROUP,
115+ 'target': targetName(tcase.volume),
116+ },
Peter Pentchevea354462023-07-18 11:15:56 +0300117+ ]
118+ if tcase.exported
119+ else []
120+ ),
Biser Milanov233be152025-01-24 17:35:11 +0200121+ }
Peter Pentchevea354462023-07-18 11:15:56 +0300122+
123+ targets = {
Biser Milanov233be152025-01-24 17:35:11 +0200124+ '0': {
125+ 'name': targetName(fake_constants.VOLUME2_ID),
126+ 'volume': volumeName(fake_constants.VOLUME2_ID),
127+ },
Peter Pentchevea354462023-07-18 11:15:56 +0300128+ }
129+ if tcase.volume is not None:
Biser Milanov233be152025-01-24 17:35:11 +0200130+ targets['1'] = {
131+ 'name': targetName(tcase.volume),
132+ 'volume': volumeName(tcase.volume),
133+ }
Peter Pentchevea354462023-07-18 11:15:56 +0300134+
Biser Milanov233be152025-01-24 17:35:11 +0200135+ return {
136+
137+ 'portalGroups': {
138+ '0': {
139+ 'name': ISCSI_PORTAL_GROUP + '-not',
140+ 'networks': [],
141+ },
142+ '1': {
143+ 'name': ISCSI_PORTAL_GROUP,
144+ 'networks': [
145+ {'address': "192.0.2.0"},
146+ {'address': "195.51.100.0"},
Peter Pentchevea354462023-07-18 11:15:56 +0300147+ ],
Biser Milanov233be152025-01-24 17:35:11 +0200148+ },
Peter Pentchevea354462023-07-18 11:15:56 +0300149+ },
Biser Milanov233be152025-01-24 17:35:11 +0200150+ 'initiators': initiators,
151+ 'targets': targets,
Peter Pentchevea354462023-07-18 11:15:56 +0300152+
Biser Milanov233be152025-01-24 17:35:11 +0200153+ }
Peter Pentchevea354462023-07-18 11:15:56 +0300154+
Peter Pentchevea354462023-07-18 11:15:56 +0300155+
Peter Pentchevea354462023-07-18 11:15:56 +0300156+class MockIscsiAPI:
157+ """Mock only the iSCSI-related calls of the StorPool API bindings."""
158+
159+ _asrt: test.TestCase
Biser Milanov233be152025-01-24 17:35:11 +0200160+ _configs: list[dict]
Peter Pentchevea354462023-07-18 11:15:56 +0300161+
162+ def __init__(
163+ self,
Biser Milanov233be152025-01-24 17:35:11 +0200164+ configs: list[dict],
Peter Pentchevea354462023-07-18 11:15:56 +0300165+ asrt: test.TestCase,
166+ ) -> None:
167+ """Store the reference to the list of iSCSI config objects."""
168+ self._asrt = asrt
169+ self._configs = configs
170+
Biser Milanov233be152025-01-24 17:35:11 +0200171+ def get_iscsi_config(self) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300172+ """Return the last version of the iSCSI configuration."""
Biser Milanov233be152025-01-24 17:35:11 +0200173+ return {'iscsi': self._configs[-1]}
Peter Pentchevea354462023-07-18 11:15:56 +0300174+
Biser Milanov233be152025-01-24 17:35:11 +0200175+ def _handle_export(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300176+ """Add an export for an initiator."""
177+ self._asrt.assertDictEqual(
178+ cmd,
179+ {
Biser Milanov233be152025-01-24 17:35:11 +0200180+ 'initiator': ISCSI_IQN_OURS,
181+ 'portalGroup': ISCSI_PORTAL_GROUP,
Biser Milanovec3bf982024-11-27 17:42:17 +0200182+ 'volumeName': volumeName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300183+ },
184+ )
Biser Milanovc6c42e02025-01-27 14:52:39 +0200185+ self._asrt.assertEqual(cfg['initiators']['1']['name'],
186+ cmd['initiator'])
Biser Milanov233be152025-01-24 17:35:11 +0200187+ self._asrt.assertListEqual(cfg['initiators']['1']['exports'], [])
Peter Pentchevea354462023-07-18 11:15:56 +0300188+
Biser Milanov233be152025-01-24 17:35:11 +0200189+ cfg['initiators'] = {
190+ **cfg['initiators'],
191+ '1': {
192+ **cfg['initiators']['1'],
193+ 'exports': [
194+ {
195+ 'portalGroup': cmd['portalGroup'],
196+ 'target': targetName(fake_constants.VOLUME_ID),
197+ },
198+ ],
Peter Pentchevea354462023-07-18 11:15:56 +0300199+ },
Biser Milanov233be152025-01-24 17:35:11 +0200200+ }
201+ return cfg
Peter Pentchevea354462023-07-18 11:15:56 +0300202+
Biser Milanov233be152025-01-24 17:35:11 +0200203+ def _handle_delete_export(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200204+ """Delete an export for an initiator."""
205+ self._asrt.assertDictEqual(
206+ cmd,
207+ {
Biser Milanov233be152025-01-24 17:35:11 +0200208+ 'initiator': ISCSI_IQN_OURS,
209+ 'portalGroup': ISCSI_PORTAL_GROUP,
Biser Milanovec3bf982024-11-27 17:42:17 +0200210+ 'volumeName': volumeName(fake_constants.VOLUME_ID),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200211+ },
212+ )
Biser Milanovc6c42e02025-01-27 14:52:39 +0200213+ self._asrt.assertEqual(cfg['initiators']['1']['name'],
214+ cmd['initiator'])
Biser Milanovd684c1c2024-11-22 12:12:59 +0200215+ self._asrt.assertListEqual(
Biser Milanov233be152025-01-24 17:35:11 +0200216+ cfg['initiators']['1']['exports'],
217+ [{'portalGroup': ISCSI_PORTAL_GROUP,
Biser Milanovc6c42e02025-01-27 14:52:39 +0200218+ 'target': cfg['targets']['1']['name']}])
Biser Milanovd684c1c2024-11-22 12:12:59 +0200219+
Biser Milanov233be152025-01-24 17:35:11 +0200220+ del cfg['initiators']['1']
221+ return cfg
Biser Milanovd684c1c2024-11-22 12:12:59 +0200222+
Biser Milanov233be152025-01-24 17:35:11 +0200223+ def _handle_create_initiator(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300224+ """Add a whole new initiator."""
225+ self._asrt.assertDictEqual(
226+ cmd,
227+ {
Biser Milanov233be152025-01-24 17:35:11 +0200228+ 'name': ISCSI_IQN_OURS,
Peter Pentchevea354462023-07-18 11:15:56 +0300229+ 'username': '',
230+ 'secret': '',
231+ },
232+ )
233+ self._asrt.assertNotIn(
234+ cmd['name'],
Biser Milanov233be152025-01-24 17:35:11 +0200235+ [init['name'] for init in cfg['initiators'].values()],
Peter Pentchevea354462023-07-18 11:15:56 +0300236+ )
Biser Milanov233be152025-01-24 17:35:11 +0200237+ self._asrt.assertListEqual(sorted(cfg['initiators']), ['0'])
Peter Pentchevea354462023-07-18 11:15:56 +0300238+
Biser Milanov233be152025-01-24 17:35:11 +0200239+ cfg['initiators'] = {
240+ **cfg['initiators'],
241+ '1': {'name': cmd['name'], 'exports': []},
242+ }
243+ return cfg
Peter Pentchevea354462023-07-18 11:15:56 +0300244+
Biser Milanov233be152025-01-24 17:35:11 +0200245+ def _handle_create_target(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300246+ """Add a target for a volume so that it may be exported."""
247+ self._asrt.assertDictEqual(
248+ cmd,
Biser Milanovec3bf982024-11-27 17:42:17 +0200249+ {'volumeName': volumeName(fake_constants.VOLUME_ID)},
Peter Pentchevea354462023-07-18 11:15:56 +0300250+ )
Biser Milanov233be152025-01-24 17:35:11 +0200251+ self._asrt.assertListEqual(sorted(cfg['targets']), ['0'])
252+ cfg['targets'] = {
253+ **cfg['targets'],
254+ '1': {
255+ 'name': targetName(fake_constants.VOLUME_ID),
256+ 'volume': volumeName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300257+ },
Biser Milanov233be152025-01-24 17:35:11 +0200258+ }
259+ return cfg
Peter Pentchevea354462023-07-18 11:15:56 +0300260+
Biser Milanov233be152025-01-24 17:35:11 +0200261+ def _handle_delete_target(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200262+ """Remove a target for a volume."""
263+ self._asrt.assertDictEqual(
264+ cmd,
Biser Milanovec3bf982024-11-27 17:42:17 +0200265+ {'volumeName': volumeName(fake_constants.VOLUME_ID)},
Biser Milanovd684c1c2024-11-22 12:12:59 +0200266+ )
267+
Biser Milanov233be152025-01-24 17:35:11 +0200268+ self._asrt.assertListEqual(sorted(cfg['targets']), ['0', '1'])
269+ del cfg['targets']['1']
270+ return cfg
Biser Milanovd684c1c2024-11-22 12:12:59 +0200271+
Peter Pentchevea354462023-07-18 11:15:56 +0300272+ def _handle_initiator_add_network(
273+ self,
Biser Milanov233be152025-01-24 17:35:11 +0200274+ cfg: dict,
Peter Pentchevea354462023-07-18 11:15:56 +0300275+ cmd: dict[str, Any],
Biser Milanov233be152025-01-24 17:35:11 +0200276+ ) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300277+ """Add a network that an initiator is allowed to log in from."""
278+ self._asrt.assertDictEqual(
279+ cmd,
280+ {
Biser Milanov233be152025-01-24 17:35:11 +0200281+ 'initiator': ISCSI_IQN_OURS,
Peter Pentchevea354462023-07-18 11:15:56 +0300282+ 'net': '0.0.0.0/0',
283+ },
284+ )
Biser Milanov233be152025-01-24 17:35:11 +0200285+ return cfg
Peter Pentchevea354462023-07-18 11:15:56 +0300286+
287+ _CMD_HANDLERS = {
288+ 'createInitiator': _handle_create_initiator,
289+ 'createTarget': _handle_create_target,
Biser Milanovd684c1c2024-11-22 12:12:59 +0200290+ 'deleteTarget': _handle_delete_target,
Peter Pentchevea354462023-07-18 11:15:56 +0300291+ 'export': _handle_export,
Biser Milanovd684c1c2024-11-22 12:12:59 +0200292+ 'exportDelete': _handle_delete_export,
Peter Pentchevea354462023-07-18 11:15:56 +0300293+ 'initiatorAddNetwork': _handle_initiator_add_network,
294+ }
295+
Biser Milanov233be152025-01-24 17:35:11 +0200296+ def post_iscsi_config(
Peter Pentchevea354462023-07-18 11:15:56 +0300297+ self,
298+ commands: dict[str, list[dict[str, dict[str, Any]]]],
299+ ) -> None:
300+ """Apply the requested changes to the iSCSI configuration.
301+
302+ This method adds a new config object to the configs list,
303+ making a shallow copy of the last one and applying the changes
304+ specified in the list of commands.
305+ """
306+ self._asrt.assertListEqual(sorted(commands), ['commands'])
307+ self._asrt.assertGreater(len(commands['commands']), 0)
308+ for cmd in commands['commands']:
309+ keys = sorted(cmd.keys())
310+ cmd_name = keys[0]
311+ self._asrt.assertListEqual(keys, [cmd_name])
312+ handler = self._CMD_HANDLERS[cmd_name]
313+ new_cfg = handler(self, self._configs[-1], cmd[cmd_name])
314+ self._configs.append(new_cfg)
315+
316+
317+_ISCSI_TEST_CASES = [
318+ IscsiTestCase(None, None, False, 4),
Biser Milanov233be152025-01-24 17:35:11 +0200319+ IscsiTestCase(ISCSI_IQN_OURS, None, False, 2),
320+ IscsiTestCase(ISCSI_IQN_OURS, fake_constants.VOLUME_ID, False, 1),
321+ IscsiTestCase(ISCSI_IQN_OURS, fake_constants.VOLUME_ID, True, 0),
Peter Pentchevea354462023-07-18 11:15:56 +0300322+]
323+
Biser Milanovc0ee3582025-01-22 09:09:24 +0000324+
Biser Milanov233be152025-01-24 17:35:11 +0200325 class MockVolumeDB(object):
326 """Simulate a Cinder database with a volume_get() method."""
327
Biser Milanovc6c42e02025-01-27 14:52:39 +0200328@@ -198,7 +449,15 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300329 self.cfg.volume_backend_name = 'storpool_test'
330 self.cfg.storpool_template = None
331 self.cfg.storpool_replication = 3
Biser Milanovd684c1c2024-11-22 12:12:59 +0200332+ self.cfg.storpool_iscsi_cinder_volume = False
333+ self.cfg.storpool_iscsi_export_to = ''
334+ self.cfg.storpool_iscsi_learn_initiator_iqns = True
Biser Milanov233be152025-01-24 17:35:11 +0200335+ self.cfg.storpool_iscsi_portal_group = ISCSI_PORTAL_GROUP
Biser Milanov233be152025-01-24 17:35:11 +0200336
Biser Milanovc6c42e02025-01-27 14:52:39 +0200337+ self._setup_test_driver()
338+
Peter Pentchev9c24be92022-09-26 22:35:24 +0300339+ def _setup_test_driver(self):
340+ """Initialize a StorPool driver as per the current configuration."""
341 mock_exec = mock.Mock()
342 mock_exec.return_value = ('', '')
343
Biser Milanovc6c42e02025-01-27 14:52:39 +0200344@@ -216,7 +475,7 @@ class StorPoolTestCase(test.TestCase):
Biser Milanov233be152025-01-24 17:35:11 +0200345 self.driver.check_for_setup_error()
Peter Pentchev9c24be92022-09-26 22:35:24 +0300346
347 @ddt.data(
348- (5, TypeError),
349+ (5, (TypeError, AttributeError)),
350 ({'no-host': None}, KeyError),
351 ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
352 ({'host': 's01'}, None),
Biser Milanovc6c42e02025-01-27 14:52:39 +0200353@@ -232,7 +491,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300354 conn)
355
356 @ddt.data(
357- (5, TypeError),
358+ (5, (TypeError, AttributeError)),
359 ({'no-host': None}, KeyError),
360 ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
361 )
Biser Milanovc6c42e02025-01-27 14:52:39 +0200362@@ -271,7 +530,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev5a9f8a62023-12-06 10:40:18 +0200363 self.assertEqual(21, pool['total_capacity_gb'])
364 self.assertEqual(5, int(pool['free_capacity_gb']))
365
366- self.assertTrue(pool['multiattach'])
367+ self.assertFalse(pool['multiattach'])
368 self.assertFalse(pool['QoS_support'])
369 self.assertFalse(pool['thick_provisioning_support'])
370 self.assertTrue(pool['thin_provisioning_support'])
Biser Milanovc6c42e02025-01-27 14:52:39 +0200371@@ -690,3 +949,179 @@ class StorPoolTestCase(test.TestCase):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200372 'No such volume',
373 self.driver.revert_to_snapshot, None,
374 {'id': vol_id}, {'id': snap_id})
Peter Pentchev9c24be92022-09-26 22:35:24 +0300375+
376+ @ddt.data(
377+ # The default values
Biser Milanov233be152025-01-24 17:35:11 +0200378+ ('', False, constants.STORPOOL, ISCSI_IQN_OURS, False),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300379+
380+ # Export to all
Biser Milanov233be152025-01-24 17:35:11 +0200381+ ('*', True, constants.ISCSI, ISCSI_IQN_OURS, True),
382+ ('*', True, constants.ISCSI, ISCSI_IQN_OURS, True),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300383+
384+ # Only export to the controller
Biser Milanov233be152025-01-24 17:35:11 +0200385+ ('', False, constants.STORPOOL, ISCSI_IQN_OURS, False),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300386+
387+ # Some of the not-fully-supported pattern lists
Biser Milanov233be152025-01-24 17:35:11 +0200388+ (ISCSI_PAT_OTHER, False, constants.STORPOOL, ISCSI_IQN_OURS, False),
389+ (ISCSI_PAT_OTHER, False, constants.STORPOOL, ISCSI_IQN_OTHER, True),
390+ (ISCSI_PAT_BOTH, False, constants.STORPOOL, ISCSI_IQN_OURS, True),
391+ (ISCSI_PAT_BOTH, False, constants.STORPOOL, ISCSI_IQN_OTHER, True),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300392+ )
393+ @ddt.unpack
Biser Milanovd684c1c2024-11-22 12:12:59 +0200394+ def test_wants_iscsi(self, storpool_iscsi_export_to, use_iscsi,
395+ storage_protocol, hostname, expected):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300396+ """Check the "should this export use iSCSI?" detection."""
Biser Milanovd684c1c2024-11-22 12:12:59 +0200397+ self.cfg.storpool_iscsi_export_to = storpool_iscsi_export_to
Peter Pentchev9c24be92022-09-26 22:35:24 +0300398+ self._setup_test_driver()
399+ self.assertEqual(self.driver._use_iscsi, use_iscsi)
400+
401+ # Make sure the driver reports the correct protocol in the stats
402+ self.driver._update_volume_stats()
403+ self.assertEqual(self.driver._stats["vendor_name"], "StorPool")
404+ self.assertEqual(self.driver._stats["storage_protocol"],
405+ storage_protocol)
406+
407+ def check(conn, forced, expected):
408+ """Pass partially or completely valid connector info."""
409+ for initiator in (None, hostname):
Biser Milanov233be152025-01-24 17:35:11 +0200410+ for host in (None, ISCSI_IQN_THIRD):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300411+ self.assertEqual(
412+ self.driver._connector_wants_iscsi({
413+ "host": host,
414+ "initiator": initiator,
415+ **conn,
416+ }),
417+ expected if initiator is not None and host is not None
418+ else forced)
419+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200420+ # If storpool_iscsi_cinder_volume is set and this is the controller,
421+ # then yes.
Peter Pentchev9c24be92022-09-26 22:35:24 +0300422+ check({"storpool_wants_iscsi": True}, True, True)
423+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200424+ # If storpool_iscsi_cinder_volume is not set or this is not the
425+ # controller, then look at the specified expected value.
Peter Pentchev9c24be92022-09-26 22:35:24 +0300426+ check({"storpool_wants_iscsi": False}, use_iscsi, expected)
427+ check({}, use_iscsi, expected)
Peter Pentchevea354462023-07-18 11:15:56 +0300428+
429+ def _validate_iscsi_config(
430+ self,
Biser Milanov233be152025-01-24 17:35:11 +0200431+ cfg: dict,
Peter Pentchevea354462023-07-18 11:15:56 +0300432+ res: dict[str, Any],
433+ tcase: IscsiTestCase,
434+ ) -> None:
435+ """Make sure the returned structure makes sense."""
436+ initiator = res['initiator']
Biser Milanov233be152025-01-24 17:35:11 +0200437+ cfg_initiator = cfg['initiators'].get('1')
Peter Pentchevea354462023-07-18 11:15:56 +0300438+
Biser Milanov233be152025-01-24 17:35:11 +0200439+ self.assertIs(res['cfg']['iscsi'], cfg)
440+ self.assertEqual(res['pg']['name'], ISCSI_PORTAL_GROUP)
Peter Pentchevea354462023-07-18 11:15:56 +0300441+
442+ if tcase.initiator is None:
443+ self.assertIsNone(initiator)
444+ else:
445+ self.assertIsNotNone(initiator)
446+ self.assertEqual(initiator, cfg_initiator)
447+
448+ if tcase.volume is None:
449+ self.assertIsNone(res['target'])
450+ else:
451+ self.assertIsNotNone(res['target'])
Biser Milanov233be152025-01-24 17:35:11 +0200452+ self.assertEqual(res['target'], cfg['targets'].get('1'))
Peter Pentchevea354462023-07-18 11:15:56 +0300453+
454+ if tcase.initiator is None:
455+ self.assertIsNone(cfg_initiator)
456+ self.assertIsNone(res['export'])
457+ else:
458+ self.assertIsNotNone(cfg_initiator)
459+ if tcase.exported:
460+ self.assertIsNotNone(res['export'])
Biser Milanov233be152025-01-24 17:35:11 +0200461+ self.assertEqual(res['export'], cfg_initiator['exports'][0])
Peter Pentchevea354462023-07-18 11:15:56 +0300462+ else:
463+ self.assertIsNone(res['export'])
464+
465+ @ddt.data(*_ISCSI_TEST_CASES)
466+ def test_iscsi_get_config(self, tcase: IscsiTestCase) -> None:
467+ """Make sure the StorPool iSCSI configuration is parsed correctly."""
468+ cfg_orig = MockIscsiConfig.build(tcase)
469+ configs = [cfg_orig]
470+ iapi = MockIscsiAPI(configs, self)
Biser Milanov233be152025-01-24 17:35:11 +0200471+ with mock.patch.object(self.driver, '_sp_api', iapi):
Peter Pentchevea354462023-07-18 11:15:56 +0300472+ res = self.driver._get_iscsi_config(
Biser Milanov233be152025-01-24 17:35:11 +0200473+ ISCSI_IQN_OURS,
Biser Milanovec3bf982024-11-27 17:42:17 +0200474+ fake_constants.VOLUME_ID,
Peter Pentchevea354462023-07-18 11:15:56 +0300475+ )
476+
477+ self._validate_iscsi_config(cfg_orig, res, tcase)
478+
479+ @ddt.data(*_ISCSI_TEST_CASES)
480+ def test_iscsi_create_export(self, tcase: IscsiTestCase) -> None:
481+ """Make sure _create_iscsi_export() makes the right API calls."""
482+ cfg_orig = MockIscsiConfig.build(tcase)
483+ configs = [cfg_orig]
484+ iapi = MockIscsiAPI(configs, self)
Biser Milanov233be152025-01-24 17:35:11 +0200485+ with mock.patch.object(self.driver, '_sp_api', iapi):
Peter Pentchevea354462023-07-18 11:15:56 +0300486+ self.driver._create_iscsi_export(
487+ {
Biser Milanovec3bf982024-11-27 17:42:17 +0200488+ 'id': fake_constants.VOLUME_ID,
489+ 'display_name': fake_constants.VOLUME_NAME,
Peter Pentchevea354462023-07-18 11:15:56 +0300490+ },
491+ {
492+ # Yeah, okay, so we cheat a little bit here...
Biser Milanov233be152025-01-24 17:35:11 +0200493+ 'host': ISCSI_IQN_OURS + '.hostname',
494+ 'initiator': ISCSI_IQN_OURS,
Peter Pentchevea354462023-07-18 11:15:56 +0300495+ },
496+ )
497+
498+ self.assertEqual(len(configs), tcase.commands_count + 1)
499+ cfg_final = configs[-1]
Biser Milanov233be152025-01-24 17:35:11 +0200500+ self.assertEqual(cfg_final['initiators']['1']['name'], ISCSI_IQN_OURS)
Peter Pentchevea354462023-07-18 11:15:56 +0300501+ self.assertEqual(
Biser Milanov233be152025-01-24 17:35:11 +0200502+ cfg_final['initiators']['1']['exports'][0]['target'],
Biser Milanovec3bf982024-11-27 17:42:17 +0200503+ targetName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300504+ )
505+ self.assertEqual(
Biser Milanov233be152025-01-24 17:35:11 +0200506+ cfg_final['targets']['1']['volume'],
Biser Milanovec3bf982024-11-27 17:42:17 +0200507+ volumeName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300508+ )
Biser Milanovd684c1c2024-11-22 12:12:59 +0200509+
510+ @ddt.data(*_ISCSI_TEST_CASES)
511+ def test_remove_iscsi_export(self, tcase: IscsiTestCase):
512+ cfg_orig = MockIscsiConfig.build(tcase)
513+ configs = [cfg_orig]
514+ iapi = MockIscsiAPI(configs, self)
515+
Biser Milanov233be152025-01-24 17:35:11 +0200516+ def _target_exists(cfg: dict, volume: str) -> bool:
517+ for name, target in cfg['targets'].items():
518+ if target['volume'] == volumeName(volume):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200519+ return True
520+ return False
521+
Biser Milanov233be152025-01-24 17:35:11 +0200522+ def _export_exists(cfg: dict, volume: str) -> bool:
523+ for name, initiator in cfg['initiators'].items():
524+ for export in initiator['exports']:
525+ if export['target'] == targetName(volume):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200526+ return True
527+ return False
528+
529+ if tcase.exported:
530+ self.assertTrue(
Biser Milanov233be152025-01-24 17:35:11 +0200531+ _target_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Biser Milanovd684c1c2024-11-22 12:12:59 +0200532+ self.assertTrue(
Biser Milanov233be152025-01-24 17:35:11 +0200533+ _export_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Biser Milanovd684c1c2024-11-22 12:12:59 +0200534+
Biser Milanov233be152025-01-24 17:35:11 +0200535+ with mock.patch.object(self.driver, '_sp_api', iapi):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200536+ self.driver._remove_iscsi_export(
537+ {
Biser Milanovec3bf982024-11-27 17:42:17 +0200538+ 'id': fake_constants.VOLUME_ID,
539+ 'display_name': fake_constants.VOLUME_NAME,
Biser Milanovd684c1c2024-11-22 12:12:59 +0200540+ },
541+ {
Biser Milanov233be152025-01-24 17:35:11 +0200542+ 'host': ISCSI_IQN_OURS + '.hostname',
543+ 'initiator': ISCSI_IQN_OURS,
Biser Milanovd684c1c2024-11-22 12:12:59 +0200544+ },
545+ )
546+
547+ self.assertFalse(
Biser Milanov233be152025-01-24 17:35:11 +0200548+ _target_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Biser Milanovd684c1c2024-11-22 12:12:59 +0200549+ self.assertFalse(
Biser Milanov233be152025-01-24 17:35:11 +0200550+ _export_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Peter Pentchevacaaa382023-02-28 11:26:13 +0200551diff --git a/cinder/volume/drivers/storpool.py b/cinder/volume/drivers/storpool.py
Biser Milanovc6c42e02025-01-27 14:52:39 +0200552index 2dc7bb6be..acfabccd6 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +0300553--- a/cinder/volume/drivers/storpool.py
554+++ b/cinder/volume/drivers/storpool.py
Biser Milanovc0ee3582025-01-22 09:09:24 +0000555@@ -15,6 +15,7 @@
Peter Pentchev9c24be92022-09-26 22:35:24 +0300556
Biser Milanovda1b0682024-11-29 09:37:10 +0200557 """StorPool block device driver"""
558
Peter Pentchev9c24be92022-09-26 22:35:24 +0300559+import fnmatch
Biser Milanovda1b0682024-11-29 09:37:10 +0200560 import platform
Biser Milanov90b5a142024-11-28 11:33:39 +0200561
Biser Milanov233be152025-01-24 17:35:11 +0200562 from os_brick.initiator import storpool_utils
563@@ -36,6 +37,32 @@ LOG = logging.getLogger(__name__)
Peter Pentchev9c24be92022-09-26 22:35:24 +0300564
565
566 storpool_opts = [
Biser Milanovd684c1c2024-11-22 12:12:59 +0200567+ cfg.BoolOpt('storpool_iscsi_cinder_volume',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300568+ default=False,
569+ help='Let the cinder-volume service use iSCSI instead of '
570+ 'the StorPool block device driver for accessing '
571+ 'StorPool volumes, e.g. when creating a volume from '
572+ 'an image or vice versa.'),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200573+ cfg.StrOpt('storpool_iscsi_export_to',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300574+ default='',
575+ help='Whether to export volumes using iSCSI. '
576+ 'An empty string (the default) makes the driver export '
577+ 'all volumes using the StorPool native network protocol. '
578+ 'The value "*" makes the driver export all volumes using '
Biser Milanovd684c1c2024-11-22 12:12:59 +0200579+ 'iSCSI (see the Cinder StorPool driver documentation for '
580+ 'how this option and ``storpool_iscsi_cinder_volume`` '
581+ 'interact). Any other value leads to an experimental '
582+ 'not fully supported configuration and is interpreted as '
Peter Pentchev9c24be92022-09-26 22:35:24 +0300583+ 'a whitespace-separated list of patterns for IQNs for '
584+ 'hosts that need volumes to be exported via iSCSI, e.g. '
585+ '"iqn.1991-05.com.microsoft:\\*" for Windows hosts.'),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200586+ cfg.BoolOpt('storpool_iscsi_learn_initiator_iqns',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300587+ default=True,
588+ help='Create a StorPool record for a new initiator as soon as '
589+ 'Cinder asks for a volume to be exported to it.'),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200590+ cfg.StrOpt('storpool_iscsi_portal_group',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300591+ default=None,
592+ help='The portal group to export volumes via iSCSI in.'),
593 cfg.StrOpt('storpool_template',
594 default=None,
595 help='The StorPool template for volumes with no type.'),
Biser Milanov233be152025-01-24 17:35:11 +0200596@@ -89,10 +116,10 @@ class StorPoolDriver(driver.VolumeDriver):
597 2.1.0 - Use the new API client in os-brick to communicate with the
598 StorPool API instead of packages `storpool` and
599 `storpool.spopenstack`
600-
601+ 2.2.0 - Add iSCSI export support.
Biser Milanovd684c1c2024-11-22 12:12:59 +0200602 """
603
Biser Milanov233be152025-01-24 17:35:11 +0200604- VERSION = '2.1.0'
605+ VERSION = '2.2.0'
Biser Milanovd684c1c2024-11-22 12:12:59 +0200606 CI_WIKI_NAME = 'StorPool_distributed_storage_CI'
607
608 def __init__(self, *args, **kwargs):
Biser Milanov233be152025-01-24 17:35:11 +0200609@@ -103,6 +130,7 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300610 self._ourIdInt = None
Biser Milanov233be152025-01-24 17:35:11 +0200611 self._sp_api = None
612 self._volume_prefix = None
Biser Milanovd684c1c2024-11-22 12:12:59 +0200613+ self._use_iscsi = False
Peter Pentchev9c24be92022-09-26 22:35:24 +0300614
615 @staticmethod
616 def get_driver_options():
Biser Milanovc6c42e02025-01-27 14:52:39 +0200617@@ -158,10 +186,327 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300618 raise StorPoolConfigurationInvalid(
619 section=hostname, param='SP_OURID', error=e)
620
621+ def _connector_wants_iscsi(self, connector):
622+ """Should we do this export via iSCSI?
623+
624+ Check the configuration to determine whether this connector is
625+ expected to provide iSCSI exports as opposed to native StorPool
626+ protocol ones. Match the initiator's IQN against the list of
Biser Milanovd684c1c2024-11-22 12:12:59 +0200627+ patterns supplied in the "storpool_iscsi_export_to" configuration
628+ setting.
Peter Pentchev9c24be92022-09-26 22:35:24 +0300629+ """
630+ if connector is None:
631+ return False
632+ if self._use_iscsi:
Biser Milanov233be152025-01-24 17:35:11 +0200633+ LOG.debug('forcing iSCSI for all exported volumes')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300634+ return True
635+ if connector.get('storpool_wants_iscsi'):
Biser Milanov233be152025-01-24 17:35:11 +0200636+ LOG.debug('forcing iSCSI for the controller')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300637+ return True
638+
Biser Milanov233be152025-01-24 17:35:11 +0200639+ iqn = connector.get('initiator', None)
640+ host = connector.get('host', None)
641+
Peter Pentchev9c24be92022-09-26 22:35:24 +0300642+ if iqn is None or host is None:
Biser Milanov233be152025-01-24 17:35:11 +0200643+ LOG.debug('this connector certainly does not want iSCSI')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300644+ return False
645+
Biser Milanov233be152025-01-24 17:35:11 +0200646+ LOG.debug('check whether %(host)s (%(iqn)s) wants iSCSI',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300647+ {
648+ 'host': host,
649+ 'iqn': iqn,
650+ })
651+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200652+ export_to = self.configuration.storpool_iscsi_export_to
Peter Pentchev9c24be92022-09-26 22:35:24 +0300653+ if export_to is None:
654+ return False
655+
656+ for pat in export_to.split():
Biser Milanov233be152025-01-24 17:35:11 +0200657+ LOG.debug('matching against %(pat)s', {'pat': pat})
Peter Pentchev9c24be92022-09-26 22:35:24 +0300658+ if fnmatch.fnmatch(iqn, pat):
Biser Milanov233be152025-01-24 17:35:11 +0200659+ LOG.debug('got it!')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300660+ return True
Biser Milanov233be152025-01-24 17:35:11 +0200661+ LOG.debug('nope')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300662+ return False
663+
664 def validate_connector(self, connector):
665+ if self._connector_wants_iscsi(connector):
666+ return True
667 return self._storpool_client_id(connector) >= 0
668
669+ def _get_iscsi_config(self, iqn, volume_id):
670+ """Get the StorPool iSCSI config items pertaining to this volume.
671+
672+ Find the elements of the StorPool iSCSI configuration tree that
673+ will be needed to create, ensure, or remove the iSCSI export of
674+ the specified volume to the specified initiator.
675+ """
Biser Milanov233be152025-01-24 17:35:11 +0200676+ cfg = self._sp_api.get_iscsi_config()
Peter Pentchev9c24be92022-09-26 22:35:24 +0300677+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200678+ pg_name = self.configuration.storpool_iscsi_portal_group
Peter Pentchev9c24be92022-09-26 22:35:24 +0300679+ pg_found = [
Biser Milanovc6c42e02025-01-27 14:52:39 +0200680+ pg for pg in
681+ cfg['iscsi']['portalGroups'].values() if pg['name'] == pg_name
Peter Pentchev9c24be92022-09-26 22:35:24 +0300682+ ]
683+ if not pg_found:
684+ raise Exception('StorPool Cinder iSCSI configuration error: '
685+ 'no portal group "{pg}"'.format(pg=pg_name))
686+ pg = pg_found[0]
687+
688+ # Do we know about this initiator?
689+ i_found = [
Biser Milanovc6c42e02025-01-27 14:52:39 +0200690+ init for init in
691+ cfg['iscsi']['initiators'].values() if init['name'] == iqn
Peter Pentchev9c24be92022-09-26 22:35:24 +0300692+ ]
693+ if i_found:
694+ initiator = i_found[0]
695+ else:
696+ initiator = None
697+
698+ # Is this volume already being exported?
Biser Milanovc6c42e02025-01-27 14:52:39 +0200699+ volname = storpool_utils.os_to_sp_volume_name(
700+ self._volume_prefix, volume_id)
Peter Pentchev9c24be92022-09-26 22:35:24 +0300701+ t_found = [
Biser Milanovc6c42e02025-01-27 14:52:39 +0200702+ tgt for tgt in
703+ cfg['iscsi']['targets'].values() if tgt['volume'] == volname
Peter Pentchev9c24be92022-09-26 22:35:24 +0300704+ ]
705+ if t_found:
706+ target = t_found[0]
707+ else:
708+ target = None
709+
710+ # OK, so is this volume being exported to this initiator?
711+ export = None
712+ if initiator is not None and target is not None:
713+ e_found = [
Biser Milanov233be152025-01-24 17:35:11 +0200714+ exp for exp in initiator['exports']
Biser Milanovc6c42e02025-01-27 14:52:39 +0200715+ if (exp['portalGroup'] == pg['name'] and
716+ exp['target'] == target['name'])
Peter Pentchev9c24be92022-09-26 22:35:24 +0300717+ ]
718+ if e_found:
719+ export = e_found[0]
720+
721+ return {
722+ 'cfg': cfg,
723+ 'pg': pg,
724+ 'initiator': initiator,
725+ 'target': target,
726+ 'export': export,
727+ 'volume_name': volname,
728+ 'volume_id': volume_id,
729+ }
730+
731+ def _create_iscsi_export(self, volume, connector):
732+ """Create (if needed) an iSCSI export for the StorPool volume."""
733+ LOG.debug(
734+ '_create_iscsi_export() invoked for volume '
735+ '"%(vol_name)s" (%(vol_id)s) connector %(connector)s',
736+ {
737+ 'vol_name': volume['display_name'],
738+ 'vol_id': volume['id'],
739+ 'connector': connector,
740+ }
741+ )
742+ iqn = connector['initiator']
743+ try:
744+ cfg = self._get_iscsi_config(iqn, volume['id'])
745+ except Exception as exc:
746+ LOG.error(
747+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
748+ )
749+ raise
750+
751+ if cfg['initiator'] is None:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200752+ if not (self.configuration.storpool_iscsi_learn_initiator_iqns or
753+ self.configuration.storpool_iscsi_cinder_volume and
Peter Pentchev9c24be92022-09-26 22:35:24 +0300754+ connector.get('storpool_wants_iscsi')):
755+ raise Exception('The "{iqn}" initiator IQN for the "{host}" '
756+ 'host is not defined in the StorPool '
757+ 'configuration.'
758+ .format(iqn=iqn, host=connector['host']))
759+ else:
760+ LOG.info('Creating a StorPool iSCSI initiator '
761+ 'for "{host}s" ({iqn}s)',
762+ {'host': connector['host'], 'iqn': iqn})
Biser Milanov233be152025-01-24 17:35:11 +0200763+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300764+ 'commands': [
765+ {
766+ 'createInitiator': {
767+ 'name': iqn,
768+ 'username': '',
769+ 'secret': '',
770+ },
771+ },
772+ {
773+ 'initiatorAddNetwork': {
774+ 'initiator': iqn,
775+ 'net': '0.0.0.0/0',
776+ },
777+ },
778+ ]
779+ })
780+
781+ if cfg['target'] is None:
782+ LOG.info(
783+ 'Creating a StorPool iSCSI target '
784+ 'for the "%(vol_name)s" volume (%(vol_id)s)',
785+ {
786+ 'vol_name': volume['display_name'],
787+ 'vol_id': volume['id'],
788+ }
789+ )
Biser Milanov233be152025-01-24 17:35:11 +0200790+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300791+ 'commands': [
792+ {
793+ 'createTarget': {
794+ 'volumeName': cfg['volume_name'],
795+ },
796+ },
797+ ]
798+ })
799+ cfg = self._get_iscsi_config(iqn, volume['id'])
800+
801+ if cfg['export'] is None:
802+ LOG.info('Creating a StorPool iSCSI export '
803+ 'for the "{vol_name}s" volume ({vol_id}s) '
804+ 'to the "{host}s" initiator ({iqn}s) '
805+ 'in the "{pg}s" portal group',
806+ {
807+ 'vol_name': volume['display_name'],
808+ 'vol_id': volume['id'],
809+ 'host': connector['host'],
810+ 'iqn': iqn,
Biser Milanov233be152025-01-24 17:35:11 +0200811+ 'pg': cfg['pg']['name']
Peter Pentchev9c24be92022-09-26 22:35:24 +0300812+ })
Biser Milanov233be152025-01-24 17:35:11 +0200813+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300814+ 'commands': [
815+ {
816+ 'export': {
817+ 'initiator': iqn,
Biser Milanov233be152025-01-24 17:35:11 +0200818+ 'portalGroup': cfg['pg']['name'],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300819+ 'volumeName': cfg['volume_name'],
820+ },
821+ },
822+ ]
823+ })
824+
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200825+ target_portals = [
Biser Milanov233be152025-01-24 17:35:11 +0200826+ "{addr}:3260".format(addr=net['address'])
827+ for net in cfg['pg']['networks']
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200828+ ]
Biser Milanov233be152025-01-24 17:35:11 +0200829+ target_iqns = [cfg['target']['name']] * len(target_portals)
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200830+ target_luns = [0] * len(target_portals)
831+ if connector.get('multipath', False):
832+ multipath_settings = {
833+ 'target_iqns': target_iqns,
834+ 'target_portals': target_portals,
835+ 'target_luns': target_luns,
836+ }
837+ else:
838+ multipath_settings = {}
839+
Peter Pentchev9c24be92022-09-26 22:35:24 +0300840+ res = {
841+ 'driver_volume_type': 'iscsi',
842+ 'data': {
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200843+ **multipath_settings,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300844+ 'target_discovered': False,
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200845+ 'target_iqn': target_iqns[0],
846+ 'target_portal': target_portals[0],
847+ 'target_lun': target_luns[0],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300848+ 'volume_id': volume['id'],
849+ 'discard': True,
850+ },
851+ }
852+ LOG.debug('returning %(res)s', {'res': res})
853+ return res
854+
855+ def _remove_iscsi_export(self, volume, connector):
856+ """Remove an iSCSI export for the specified StorPool volume."""
857+ LOG.debug(
858+ '_remove_iscsi_export() invoked for volume '
859+ '"%(vol_name)s" (%(vol_id)s) connector %(conn)s',
860+ {
861+ 'vol_name': volume['display_name'],
862+ 'vol_id': volume['id'],
863+ 'conn': connector,
864+ }
865+ )
866+ try:
867+ cfg = self._get_iscsi_config(connector['initiator'], volume['id'])
868+ except Exception as exc:
869+ LOG.error(
870+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
871+ )
872+ raise
873+
874+ if cfg['export'] is not None:
875+ LOG.info('Removing the StorPool iSCSI export '
876+ 'for the "%(vol_name)s" volume (%(vol_id)s) '
877+ 'to the "%(host)s" initiator (%(iqn)s) '
878+ 'in the "%(pg)s" portal group',
879+ {
880+ 'vol_name': volume['display_name'],
881+ 'vol_id': volume['id'],
882+ 'host': connector['host'],
883+ 'iqn': connector['initiator'],
Biser Milanov233be152025-01-24 17:35:11 +0200884+ 'pg': cfg['pg']['name'],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300885+ })
886+ try:
Biser Milanov233be152025-01-24 17:35:11 +0200887+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300888+ 'commands': [
889+ {
890+ 'exportDelete': {
Biser Milanov233be152025-01-24 17:35:11 +0200891+ 'initiator': cfg['initiator']['name'],
892+ 'portalGroup': cfg['pg']['name'],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300893+ 'volumeName': cfg['volume_name'],
894+ },
895+ },
896+ ]
897+ })
Biser Milanov233be152025-01-24 17:35:11 +0200898+ except storpool_utils.StorPoolAPIError as e:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300899+ if e.name not in ('objectExists', 'objectDoesNotExist'):
900+ raise
901+ LOG.info('Looks like somebody beat us to it')
902+
903+ if cfg['target'] is not None:
904+ last = True
Biser Milanov233be152025-01-24 17:35:11 +0200905+ for initiator in cfg['cfg']['iscsi']['initiators'].values():
906+ if initiator['name'] == cfg['initiator']['name']:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300907+ continue
Biser Milanov233be152025-01-24 17:35:11 +0200908+ for exp in initiator['exports']:
909+ if exp['target'] == cfg['target']['name']:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300910+ last = False
911+ break
912+ if not last:
913+ break
914+
915+ if last:
916+ LOG.info(
917+ 'Removing the StorPool iSCSI target '
918+ 'for the "{vol_name}s" volume ({vol_id}s)',
919+ {
920+ 'vol_name': volume['display_name'],
921+ 'vol_id': volume['id'],
922+ }
923+ )
924+ try:
Biser Milanov233be152025-01-24 17:35:11 +0200925+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300926+ 'commands': [
927+ {
928+ 'deleteTarget': {
929+ 'volumeName': cfg['volume_name'],
930+ },
931+ },
932+ ]
933+ })
Biser Milanov233be152025-01-24 17:35:11 +0200934+ except storpool_utils.StorPoolAPIError as e:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300935+ if e.name not in ('objectDoesNotExist', 'invalidParam'):
936+ raise
937+ LOG.info('Looks like somebody beat us to it')
938+
939 def initialize_connection(self, volume, connector):
940+ if self._connector_wants_iscsi(connector):
941+ return self._create_iscsi_export(volume, connector)
942 return {'driver_volume_type': 'storpool',
943 'data': {
944 'client_id': self._storpool_client_id(connector),
Biser Milanovc6c42e02025-01-27 14:52:39 +0200945@@ -170,6 +515,9 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300946 }}
947
948 def terminate_connection(self, volume, connector, **kwargs):
949+ if self._connector_wants_iscsi(connector):
Biser Milanov233be152025-01-24 17:35:11 +0200950+ LOG.debug('removing an iSCSI export')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300951+ self._remove_iscsi_export(volume, connector)
952 pass
953
954 def create_snapshot(self, snapshot):
Biser Milanovc6c42e02025-01-27 14:52:39 +0200955@@ -278,11 +626,20 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300956 )
957
958 def create_export(self, context, volume, connector):
959- pass
960+ if self._connector_wants_iscsi(connector):
Biser Milanov233be152025-01-24 17:35:11 +0200961+ LOG.debug('creating an iSCSI export')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300962+ self._create_iscsi_export(volume, connector)
963
964 def remove_export(self, context, volume):
965 pass
966
967+ def _attach_volume(self, context, volume, properties, remote=False):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200968+ if self.configuration.storpool_iscsi_cinder_volume and not remote:
Biser Milanov233be152025-01-24 17:35:11 +0200969+ LOG.debug('adding the "storpool_wants_iscsi" flag')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300970+ properties['storpool_wants_iscsi'] = True
971+
972+ return super()._attach_volume(context, volume, properties, remote)
973+
974 def delete_volume(self, volume):
Biser Milanov233be152025-01-24 17:35:11 +0200975 name = storpool_utils.os_to_sp_volume_name(
976 self._volume_prefix, volume['id'])
Biser Milanovc6c42e02025-01-27 14:52:39 +0200977@@ -321,6 +678,17 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300978 LOG.error("StorPoolDriver API initialization failed: %s", e)
979 raise
980
Biser Milanovd684c1c2024-11-22 12:12:59 +0200981+ export_to = self.configuration.storpool_iscsi_export_to
Peter Pentchev9c24be92022-09-26 22:35:24 +0300982+ export_to_set = export_to is not None and export_to.split()
Biser Milanovd684c1c2024-11-22 12:12:59 +0200983+ vol_iscsi = self.configuration.storpool_iscsi_cinder_volume
984+ pg_name = self.configuration.storpool_iscsi_portal_group
Peter Pentchev9c24be92022-09-26 22:35:24 +0300985+ if (export_to_set or vol_iscsi) and pg_name is None:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200986+ msg = _('The "storpool_iscsi_portal_group" option is required if '
987+ 'any patterns are listed in "storpool_iscsi_export_to"')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300988+ raise exception.VolumeDriverException(message=msg)
989+
990+ self._use_iscsi = export_to == "*"
991+
992 def _update_volume_stats(self):
993 try:
Biser Milanov233be152025-01-24 17:35:11 +0200994 dl = self._sp_api.disks_list()
Biser Milanovc6c42e02025-01-27 14:52:39 +0200995@@ -346,7 +714,7 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300996 'total_capacity_gb': total / units.Gi,
997 'free_capacity_gb': free / units.Gi,
998 'reserved_percentage': 0,
999- 'multiattach': True,
Peter Pentchev5a9f8a62023-12-06 10:40:18 +02001000+ 'multiattach': self._use_iscsi,
Peter Pentchev9c24be92022-09-26 22:35:24 +03001001 'QoS_support': False,
1002 'thick_provisioning_support': False,
1003 'thin_provisioning_support': True,
Biser Milanovc6c42e02025-01-27 14:52:39 +02001004@@ -365,7 +733,9 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001005 'volume_backend_name') or 'storpool',
1006 'vendor_name': 'StorPool',
1007 'driver_version': self.VERSION,
1008- 'storage_protocol': constants.STORPOOL,
1009+ 'storage_protocol': (
1010+ constants.ISCSI if self._use_iscsi else constants.STORPOOL
1011+ ),
Peter Pentchevacaaa382023-02-28 11:26:13 +02001012 # Driver capabilities
Peter Pentchev9c24be92022-09-26 22:35:24 +03001013 'clone_across_pools': True,
1014 'sparse_copy_volume': True,
Biser Milanovc6c42e02025-01-27 14:52:39 +02001015@@ -452,15 +822,15 @@ class StorPoolDriver(driver.VolumeDriver):
Biser Milanov233be152025-01-24 17:35:11 +02001016 LOG.debug('Trying to swap volume names, intermediate "%(int)s"',
1017 {'int': int_name})
1018 try:
1019- LOG.debug('- rename "%(orig)s" to "%(int)s"',
1020+ LOG.debug('rename "%(orig)s" to "%(int)s"',
1021 {'orig': orig_name, 'int': int_name})
1022 self._sp_api.volume_update(orig_name, {'rename': int_name})
1023
1024- LOG.debug('- rename "%(temp)s" to "%(orig)s"',
1025+ LOG.debug('rename "%(temp)s" to "%(orig)s"',
1026 {'temp': temp_name, 'orig': orig_name})
1027 self._sp_api.volume_update(temp_name, {'rename': orig_name})
1028
1029- LOG.debug('- rename "%(int)s" to "%(temp)s"',
1030+ LOG.debug('rename "%(int)s" to "%(temp)s"',
1031 {'int': int_name, 'temp': temp_name})
1032 self._sp_api.volume_update(int_name, {'rename': temp_name})
1033 return {'_name_id': None}
Peter Pentchevacaaa382023-02-28 11:26:13 +02001034diff --git a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
Biser Milanovc0ee3582025-01-22 09:09:24 +00001035index d2c5895a9..936e83675 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +03001036--- a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
1037+++ b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
Peter Pentchevacaaa382023-02-28 11:26:13 +02001038@@ -19,12 +19,15 @@ Prerequisites
Peter Pentchev9c24be92022-09-26 22:35:24 +03001039 * The controller and all the compute nodes must have access to the StorPool
1040 API service.
1041
1042-* All nodes where StorPool-backed volumes will be attached must have access to
1043+* If iSCSI is not being used as a transport (see below), all nodes where
1044+ StorPool-backed volumes will be attached must have access to
1045 the StorPool data network and run the ``storpool_block`` service.
1046
1047-* If StorPool-backed Cinder volumes need to be created directly from Glance
1048- images, then the node running the ``cinder-volume`` service must also have
1049- access to the StorPool data network and run the ``storpool_block`` service.
1050+* If Glance uses Cinder as its image store, or if StorPool-backed Cinder
1051+ volumes need to be created directly from Glance images, and iSCSI is not
1052+ being used as a transport, then the node running the ``cinder-volume``
1053+ service must also have access to the StorPool data network and run
1054+ the ``storpool_block`` service.
1055
1056 * All nodes that need to access the StorPool API (the compute nodes and
1057 the node running the ``cinder-volume`` service) must have the following
Biser Milanovc0ee3582025-01-22 09:09:24 +00001058@@ -34,6 +37,34 @@ Prerequisites
Peter Pentchev9c24be92022-09-26 22:35:24 +03001059 * the storpool Python bindings package
1060 * the storpool.spopenstack Python helper package
1061
1062+Using iSCSI as the transport protocol
1063+-------------------------------------
1064+
1065+The StorPool distributed storage system uses its own, highly optimized and
1066+tailored for its specifics, network protocol for communication between
1067+the storage servers and the clients (the OpenStack cluster nodes where
1068+StorPool-backed volumes will be attached). There are cases when granting
1069+various nodes access to the StorPool data network or installing and
1070+running the ``storpool_block`` client service on them may pose difficulties.
1071+The StorPool servers may also expose the user-created volumes and snapshots
1072+using the standard iSCSI protocol that only requires TCP routing and
1073+connectivity between the storage servers and the StorPool clients.
1074+The StorPool Cinder driver may be configured to export volumes and
Biser Milanovc0ee3582025-01-22 09:09:24 +00001075+snapshots via iSCSI using the ``storpool_iscsi_export_to`` and
1076+``storpool_iscsi_portal_group`` configuration options.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001077+
1078+Additionally, even if e.g. the hypervisor nodes running Nova will use
1079+the StorPool network protocol and run the ``storpool_block`` service
Biser Milanovc0ee3582025-01-22 09:09:24 +00001080+(so the ``storpool_iscsi_export_to`` option has its default empty string
1081+value), the ``storpool_iscsi_cinder_volume`` option configures the
1082+StorPool Cinder driver so that only the ``cinder-volume`` service will
1083+use the iSCSI protocol when attaching volumes and snapshots to transfer
1084+data to and from Glance images.
Biser Milanovd684c1c2024-11-22 12:12:59 +02001085+
1086+Multiattach support for StorPool is only enabled if iSCSI is used:
Biser Milanovc0ee3582025-01-22 09:09:24 +00001087+``storpool_iscsi_export_to`` is set to ``*``, that is, when all StorPool
1088+volumes will be exported via iSCSI to all initiators.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001089+
1090 Configuring the StorPool volume driver
1091 --------------------------------------
1092
Biser Milanovc0ee3582025-01-22 09:09:24 +00001093@@ -55,6 +86,35 @@ volume backend definition) and per volume type:
Peter Pentchev9c24be92022-09-26 22:35:24 +03001094 with the default placement constraints for the StorPool cluster.
1095 The default value for the chain replication is 3.
1096
Peter Pentchevea354462023-07-18 11:15:56 +03001097+In addition, if the iSCSI protocol is used to access the StorPool cluster as
1098+described in the previous section, the following options may be defined in
1099+the ``cinder.conf`` volume backend definition:
1100+
Biser Milanovc0ee3582025-01-22 09:09:24 +00001101+- ``storpool_iscsi_export_to``: if set to the value ``*``, the StorPool
1102+ Cinder driver will export volumes and snapshots using the iSCSI
1103+ protocol instead of the StorPool network protocol. The
1104+ ``storpool_iscsi_portal_group`` option must also be specified.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001105+
Biser Milanovc0ee3582025-01-22 09:09:24 +00001106+- ``storpool_iscsi_portal_group``: if the ``storpool_iscsi_export_to``
1107+ option is set to the value ``*`` or the
1108+ ``storpool_iscsi_cinder_volume`` option is turned on, this option
1109+ specifies the name of the iSCSI portal group that Cinder volumes will
1110+ be exported to.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001111+
Biser Milanovc0ee3582025-01-22 09:09:24 +00001112+- ``storpool_iscsi_cinder_volume``: if enabled, even if the
1113+ ``storpool_iscsi_export_to`` option has its default empty value, the
1114+ ``cinder-volume`` service will use iSCSI to attach the volumes and
1115+ snapshots for transferring data to and from Glance images if Glance is
1116+ configured to use the Cinder glance_store.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001117+
Biser Milanovc0ee3582025-01-22 09:09:24 +00001118+- ``storpool_iscsi_learn_initiator_iqns``: if enabled, the StorPool
1119+ Cinder driver will automatically use the StorPool API to create
1120+ definitions for new initiators in the StorPool cluster's
1121+ configuration. This is the default behavior of the driver; it may be
1122+ disabled in the rare case if, e.g. because of site policy, OpenStack
1123+ iSCSI initiators (e.g. Nova hypervisors) need to be explicitly allowed
1124+ to use the StorPool iSCSI targets.
Peter Pentchevea354462023-07-18 11:15:56 +03001125+
Peter Pentchev9c24be92022-09-26 22:35:24 +03001126 Using the StorPool volume driver
1127 --------------------------------
1128
Biser Milanovc0ee3582025-01-22 09:09:24 +00001129diff --git a/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml b/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
1130new file mode 100644
1131index 000000000..3863e4099
1132--- /dev/null
1133+++ b/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
1134@@ -0,0 +1,15 @@
1135+features:
1136+ - |
1137+ StorPool driver: Added support for exporting the StorPool-backed
1138+ volumes using the iSCSI protocol, so that the Cinder volume service
1139+ and/or the Nova or Glance consumers do not need to have the StorPool
1140+ block device third-party service installed. See the StorPool driver
1141+ section in the Cinder documentation for more information on the
1142+ ``storpool_iscsi_export_to``, ``storpool_iscsi_portal_group``,
1143+ ``storpool_iscsi_cinder_volume``, and
1144+ ``storpool_iscsi_learn_initiator_iqns`` options.
1145+
1146+ .. note::
1147+ Multiattach support for StorPool is now only enabled if
1148+ ``storpool_iscsi_export_to`` is set to ``*``, that is, when all
1149+ StorPool volumes will be exported via iSCSI to all initiators.
1150--
11512.43.0
1152