blob: 819055a6515483a695aa038b2db2b146f98e1950 [file] [log] [blame]
Biser Milanov8323a8c2024-11-20 10:53:27 +02001From 06914652d6de93e3d14209ff6a78b930826f0823 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
Peter Pentchev5cf673c2024-02-20 10:04:41 +02004Subject: [PATCH 9/9] Add iSCSI export support to the StorPool driver
Peter Pentchev9c24be92022-09-26 22:35:24 +03005
6Add four new driver options:
7- iscsi_cinder_volume: use StorPool iSCSI attachments whenever
8 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
10- iscsi_export_to:
11 - an empty string to use the StorPool native protocol for exporting volumes
12 protocol for exporting volumes)
13 - 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
17- iscsi_portal_group: the name of the iSCSI portal group defined in
18 the StorPool configuration to use for these export
19- iscsi_learn_initiator_iqns: automatically create StorPool configuration
20 records for an initiator when a volume is first exported to it
21
22When exporting volumes via iSCSI, report the storage protocol as "iSCSI" and
23disable multiattach (the StorPool CI failures with iSCSI multiattach may need
24further investigation).
25
26Change-Id: I9de64306e0e6976268df782053b0651dd1cca96f
27---
Biser Milanov8323a8c2024-11-20 10:53:27 +020028 .../unit/volume/drivers/test_storpool.py | 437 +++++++++++++++++-
29 cinder/volume/drivers/storpool.py | 374 ++++++++++++++-
30 .../drivers/storpool-volume-driver.rst | 60 ++-
31 .../storpool-iscsi-cefcfe590a07c5c7.yaml | 13 +
32 4 files changed, 874 insertions(+), 10 deletions(-)
33 create mode 100644 releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
Peter Pentchev9c24be92022-09-26 22:35:24 +030034
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 Milanov8323a8c2024-11-20 10:53:27 +020036index a6c894ab9..78c9f14a8 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 Milanov8323a8c2024-11-20 10:53:27 +020039@@ -14,14 +14,24 @@
40 # under the License.
Peter Pentchevea354462023-07-18 11:15:56 +030041
42
Biser Milanov8323a8c2024-11-20 10:53:27 +020043+from __future__ import annotations
44+
Peter Pentchevea354462023-07-18 11:15:56 +030045+import dataclasses
46 import itertools
47 import re
Biser Milanov8323a8c2024-11-20 10:53:27 +020048 import sys
Peter Pentchevea354462023-07-18 11:15:56 +030049+from typing import Any, NamedTuple, TYPE_CHECKING # noqa: H301
50 from unittest import mock
51
52 import ddt
53 from oslo_utils import units
Peter Pentchevea354462023-07-18 11:15:56 +030054
Biser Milanov8323a8c2024-11-20 10:53:27 +020055+if TYPE_CHECKING:
56+ if sys.version_info >= (3, 11):
57+ from typing import Self
58+ else:
59+ from typing_extensions import Self
60+
61
62 fakeStorPool = mock.Mock()
63 fakeStorPool.spopenstack = mock.Mock()
64@@ -31,12 +41,21 @@ fakeStorPool.sptypes = mock.Mock()
65 sys.modules['storpool'] = fakeStorPool
66
67
Peter Pentchev9c24be92022-09-26 22:35:24 +030068+from cinder.common import constants
69 from cinder import exception
Peter Pentchevea354462023-07-18 11:15:56 +030070+from cinder.tests.unit import fake_constants as fconst
Peter Pentchev9c24be92022-09-26 22:35:24 +030071 from cinder.tests.unit import test
72 from cinder.volume import configuration as conf
Peter Pentchevea354462023-07-18 11:15:56 +030073 from cinder.volume.drivers import storpool as driver
Biser Milanov8323a8c2024-11-20 10:53:27 +020074
Peter Pentchevea354462023-07-18 11:15:56 +030075
76+_ISCSI_IQN_OURS = 'beleriand'
77+_ISCSI_IQN_OTHER = 'rohan'
78+_ISCSI_IQN_THIRD = 'gondor'
79+_ISCSI_PAT_OTHER = 'roh*'
80+_ISCSI_PAT_BOTH = '*riand roh*'
81+_ISCSI_PORTAL_GROUP = 'openstack_pg'
Biser Milanov8323a8c2024-11-20 10:53:27 +020082+
Peter Pentchevea354462023-07-18 11:15:56 +030083 volume_types = {
84 1: {},
Biser Milanov8323a8c2024-11-20 10:53:27 +020085 2: {'storpool_template': 'ssd'},
86@@ -73,6 +92,10 @@ def snapshotName(vtype, vid):
87 return 'os--snap--{t}--{id}'.format(t=vtype, id=vid)
Peter Pentchevea354462023-07-18 11:15:56 +030088
89
90+def targetName(vid):
91+ return 'iqn.2012-11.storpool:{id}'.format(id=vid)
92+
93+
Biser Milanov8323a8c2024-11-20 10:53:27 +020094 class MockDisk(object):
95 def __init__(self, diskId):
96 self.id = diskId
97@@ -197,6 +220,273 @@ def MockVolumeUpdateDesc(size):
98 return {'size': size}
Peter Pentchevea354462023-07-18 11:15:56 +030099
Biser Milanov8323a8c2024-11-20 10:53:27 +0200100
101+@dataclasses.dataclass(frozen=True)
102+class MockIscsiNetwork:
103+ """Mock a StorPool IP CIDR network definition (partially)."""
104+
105+ address: str
106+
107+
108+@dataclasses.dataclass(frozen=True)
109+class MockIscsiPortalGroup:
110+ """Mock a StorPool iSCSI portal group definition (partially)."""
111+
112+ name: str
113+ networks: list[MockIscsiNetwork]
114+
115+
116+@dataclasses.dataclass(frozen=True)
117+class MockIscsiExport:
118+ """Mock a StorPool iSCSI exported volume/target definition."""
119+
120+ portalGroup: str
121+ target: str
122+
123+
124+@dataclasses.dataclass(frozen=True)
125+class MockIscsiInitiator:
126+ """Mock a StorPool iSCSI initiator definition."""
127+
128+ name: str
129+ exports: list[MockIscsiExport]
130+
131+
132+@dataclasses.dataclass(frozen=True)
133+class MockIscsiTarget:
134+ """Mock a StorPool iSCSI volume-to-target mapping definition."""
135+
136+ name: str
137+ volume: str
138+
139+
Peter Pentchevea354462023-07-18 11:15:56 +0300140+class IscsiTestCase(NamedTuple):
141+ """A single test case for the iSCSI config and export methods."""
142+
143+ initiator: str | None
144+ volume: str | None
145+ exported: bool
146+ commands_count: int
147+
148+
149+@dataclasses.dataclass(frozen=True)
150+class MockIscsiConfig:
151+ """Mock the structure returned by the "get current config" query."""
152+
Biser Milanov8323a8c2024-11-20 10:53:27 +0200153+ portalGroups: dict[str, MockIscsiPortalGroup]
154+ initiators: dict[str, MockIscsiInitiator]
155+ targets: dict[str, MockIscsiTarget]
156+
Peter Pentchevea354462023-07-18 11:15:56 +0300157+ @classmethod
Biser Milanov8323a8c2024-11-20 10:53:27 +0200158+ def build(cls, tcase: IscsiTestCase) -> Self:
Peter Pentchevea354462023-07-18 11:15:56 +0300159+ """Build a test config structure."""
160+ initiators = {
Biser Milanov8323a8c2024-11-20 10:53:27 +0200161+ '0': MockIscsiInitiator(name=_ISCSI_IQN_OTHER, exports=[]),
Peter Pentchevea354462023-07-18 11:15:56 +0300162+ }
163+ if tcase.initiator is not None:
Biser Milanov8323a8c2024-11-20 10:53:27 +0200164+ initiators['1'] = MockIscsiInitiator(
165+ name=tcase.initiator,
166+ exports=(
Peter Pentchevea354462023-07-18 11:15:56 +0300167+ [
Biser Milanov8323a8c2024-11-20 10:53:27 +0200168+ MockIscsiExport(
169+ portalGroup=_ISCSI_PORTAL_GROUP,
170+ target=targetName(tcase.volume),
171+ ),
Peter Pentchevea354462023-07-18 11:15:56 +0300172+ ]
173+ if tcase.exported
174+ else []
175+ ),
Biser Milanov8323a8c2024-11-20 10:53:27 +0200176+ )
Peter Pentchevea354462023-07-18 11:15:56 +0300177+
178+ targets = {
Biser Milanov8323a8c2024-11-20 10:53:27 +0200179+ '0': MockIscsiTarget(
180+ name=targetName(fconst.VOLUME2_ID),
181+ volume=volumeName(fconst.VOLUME2_ID),
182+ ),
Peter Pentchevea354462023-07-18 11:15:56 +0300183+ }
184+ if tcase.volume is not None:
Biser Milanov8323a8c2024-11-20 10:53:27 +0200185+ targets['1'] = MockIscsiTarget(
186+ name=targetName(tcase.volume),
187+ volume=volumeName(tcase.volume),
188+ )
Peter Pentchevea354462023-07-18 11:15:56 +0300189+
Biser Milanov8323a8c2024-11-20 10:53:27 +0200190+ return cls(
191+ portalGroups={
192+ '0': MockIscsiPortalGroup(
193+ name=_ISCSI_PORTAL_GROUP + '-not',
194+ networks=[],
195+ ),
196+ '1': MockIscsiPortalGroup(
197+ name=_ISCSI_PORTAL_GROUP,
198+ networks=[
199+ MockIscsiNetwork(address="192.0.2.0"),
200+ MockIscsiNetwork(address="195.51.100.0"),
Peter Pentchevea354462023-07-18 11:15:56 +0300201+ ],
Biser Milanov8323a8c2024-11-20 10:53:27 +0200202+ ),
Peter Pentchevea354462023-07-18 11:15:56 +0300203+ },
Biser Milanov8323a8c2024-11-20 10:53:27 +0200204+ initiators=initiators,
205+ targets=targets,
206+ )
Peter Pentchevea354462023-07-18 11:15:56 +0300207+
208+
Biser Milanov8323a8c2024-11-20 10:53:27 +0200209+@dataclasses.dataclass(frozen=True)
210+class MockIscsiConfigTop:
211+ """Mock the top level of the "get the iSCSI configuration" response."""
212+
213+ iscsi: MockIscsiConfig
Peter Pentchevea354462023-07-18 11:15:56 +0300214+
215+
216+class MockIscsiAPI:
217+ """Mock only the iSCSI-related calls of the StorPool API bindings."""
218+
219+ _asrt: test.TestCase
Biser Milanov8323a8c2024-11-20 10:53:27 +0200220+ _configs: list[MockIscsiConfig]
Peter Pentchevea354462023-07-18 11:15:56 +0300221+
222+ def __init__(
223+ self,
Biser Milanov8323a8c2024-11-20 10:53:27 +0200224+ configs: list[MockIscsiConfig],
Peter Pentchevea354462023-07-18 11:15:56 +0300225+ asrt: test.TestCase,
226+ ) -> None:
227+ """Store the reference to the list of iSCSI config objects."""
228+ self._asrt = asrt
229+ self._configs = configs
230+
Biser Milanov8323a8c2024-11-20 10:53:27 +0200231+ def iSCSIConfig(self) -> MockIscsiConfigTop:
Peter Pentchevea354462023-07-18 11:15:56 +0300232+ """Return the last version of the iSCSI configuration."""
Biser Milanov8323a8c2024-11-20 10:53:27 +0200233+ return MockIscsiConfigTop(iscsi=self._configs[-1])
Peter Pentchevea354462023-07-18 11:15:56 +0300234+
Biser Milanov8323a8c2024-11-20 10:53:27 +0200235+ def _handle_export(
236+ self,
237+ cfg: MockIscsiConfig, cmd: dict[str, Any],
238+ ) -> MockIscsiConfig:
Peter Pentchevea354462023-07-18 11:15:56 +0300239+ """Add an export for an initiator."""
240+ self._asrt.assertDictEqual(
241+ cmd,
242+ {
243+ 'initiator': _ISCSI_IQN_OURS,
244+ 'portalGroup': _ISCSI_PORTAL_GROUP,
245+ 'volumeName': volumeName(fconst.VOLUME_ID),
246+ },
247+ )
Biser Milanov8323a8c2024-11-20 10:53:27 +0200248+ self._asrt.assertEqual(cfg.initiators['1'].name, cmd['initiator'])
249+ self._asrt.assertListEqual(cfg.initiators['1'].exports, [])
Peter Pentchevea354462023-07-18 11:15:56 +0300250+
Biser Milanov8323a8c2024-11-20 10:53:27 +0200251+ return dataclasses.replace(
252+ cfg,
253+ initiators={
254+ **cfg.initiators,
255+ '1': dataclasses.replace(
256+ cfg.initiators['1'],
257+ exports=[
258+ MockIscsiExport(
259+ portalGroup=cmd['portalGroup'],
260+ target=targetName(fconst.VOLUME_ID),
261+ ),
262+ ],
263+ ),
Peter Pentchevea354462023-07-18 11:15:56 +0300264+ },
265+ )
266+
Biser Milanov8323a8c2024-11-20 10:53:27 +0200267+ def _handle_create_initiator(
268+ self,
269+ cfg: MockIscsiConfig,
270+ cmd: dict[str, Any],
271+ ) -> MockIscsiConfig:
Peter Pentchevea354462023-07-18 11:15:56 +0300272+ """Add a whole new initiator."""
273+ self._asrt.assertDictEqual(
274+ cmd,
275+ {
276+ 'name': _ISCSI_IQN_OURS,
277+ 'username': '',
278+ 'secret': '',
279+ },
280+ )
281+ self._asrt.assertNotIn(
282+ cmd['name'],
Biser Milanov8323a8c2024-11-20 10:53:27 +0200283+ [init.name for init in cfg.initiators.values()],
Peter Pentchevea354462023-07-18 11:15:56 +0300284+ )
Biser Milanov8323a8c2024-11-20 10:53:27 +0200285+ self._asrt.assertListEqual(sorted(cfg.initiators), ['0'])
Peter Pentchevea354462023-07-18 11:15:56 +0300286+
Biser Milanov8323a8c2024-11-20 10:53:27 +0200287+ return dataclasses.replace(
288+ cfg,
289+ initiators={
290+ **cfg.initiators,
291+ '1': MockIscsiInitiator(name=cmd['name'], exports=[]),
292+ },
293+ )
Peter Pentchevea354462023-07-18 11:15:56 +0300294+
Biser Milanov8323a8c2024-11-20 10:53:27 +0200295+ def _handle_create_target(
296+ self,
297+ cfg: MockIscsiConfig,
298+ cmd: dict[str, Any],
299+ ) -> MockIscsiConfig:
Peter Pentchevea354462023-07-18 11:15:56 +0300300+ """Add a target for a volume so that it may be exported."""
301+ self._asrt.assertDictEqual(
302+ cmd,
303+ {'volumeName': volumeName(fconst.VOLUME_ID)},
304+ )
Biser Milanov8323a8c2024-11-20 10:53:27 +0200305+ self._asrt.assertListEqual(sorted(cfg.targets), ['0'])
306+ return dataclasses.replace(
307+ cfg,
308+ targets={
309+ **cfg.targets,
310+ '1': MockIscsiTarget(
311+ name=targetName(fconst.VOLUME_ID),
312+ volume=volumeName(fconst.VOLUME_ID),
313+ ),
Peter Pentchevea354462023-07-18 11:15:56 +0300314+ },
315+ )
316+
317+ def _handle_initiator_add_network(
318+ self,
Biser Milanov8323a8c2024-11-20 10:53:27 +0200319+ cfg: MockIscsiConfig,
Peter Pentchevea354462023-07-18 11:15:56 +0300320+ cmd: dict[str, Any],
Biser Milanov8323a8c2024-11-20 10:53:27 +0200321+ ) -> MockIscsiConfig:
Peter Pentchevea354462023-07-18 11:15:56 +0300322+ """Add a network that an initiator is allowed to log in from."""
323+ self._asrt.assertDictEqual(
324+ cmd,
325+ {
326+ 'initiator': _ISCSI_IQN_OURS,
327+ 'net': '0.0.0.0/0',
328+ },
329+ )
Biser Milanov8323a8c2024-11-20 10:53:27 +0200330+ return dataclasses.replace(cfg)
Peter Pentchevea354462023-07-18 11:15:56 +0300331+
332+ _CMD_HANDLERS = {
333+ 'createInitiator': _handle_create_initiator,
334+ 'createTarget': _handle_create_target,
335+ 'export': _handle_export,
336+ 'initiatorAddNetwork': _handle_initiator_add_network,
337+ }
338+
Biser Milanov8323a8c2024-11-20 10:53:27 +0200339+ def iSCSIConfigChange(
Peter Pentchevea354462023-07-18 11:15:56 +0300340+ self,
341+ commands: dict[str, list[dict[str, dict[str, Any]]]],
342+ ) -> None:
343+ """Apply the requested changes to the iSCSI configuration.
344+
345+ This method adds a new config object to the configs list,
346+ making a shallow copy of the last one and applying the changes
347+ specified in the list of commands.
348+ """
349+ self._asrt.assertListEqual(sorted(commands), ['commands'])
350+ self._asrt.assertGreater(len(commands['commands']), 0)
351+ for cmd in commands['commands']:
352+ keys = sorted(cmd.keys())
353+ cmd_name = keys[0]
354+ self._asrt.assertListEqual(keys, [cmd_name])
355+ handler = self._CMD_HANDLERS[cmd_name]
356+ new_cfg = handler(self, self._configs[-1], cmd[cmd_name])
357+ self._configs.append(new_cfg)
358+
359+
360+_ISCSI_TEST_CASES = [
361+ IscsiTestCase(None, None, False, 4),
362+ IscsiTestCase(_ISCSI_IQN_OURS, None, False, 2),
363+ IscsiTestCase(_ISCSI_IQN_OURS, fconst.VOLUME_ID, False, 1),
364+ IscsiTestCase(_ISCSI_IQN_OURS, fconst.VOLUME_ID, True, 0),
365+]
366+
Biser Milanov8323a8c2024-11-20 10:53:27 +0200367+
Peter Pentchevea354462023-07-18 11:15:56 +0300368 def MockSPConfig(section = 's01'):
369 res = {}
Biser Milanov8323a8c2024-11-20 10:53:27 +0200370 m = re.match('^s0*([A-Za-z0-9]+)$', section)
371@@ -239,7 +529,15 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300372 self.cfg.volume_backend_name = 'storpool_test'
373 self.cfg.storpool_template = None
374 self.cfg.storpool_replication = 3
375+ self.cfg.iscsi_cinder_volume = False
376+ self.cfg.iscsi_export_to = ''
Peter Pentchevea354462023-07-18 11:15:56 +0300377+ self.cfg.iscsi_learn_initiator_iqns = True
378+ self.cfg.iscsi_portal_group = _ISCSI_PORTAL_GROUP
Peter Pentchev9c24be92022-09-26 22:35:24 +0300379+
Peter Pentchevea354462023-07-18 11:15:56 +0300380+ self._setup_test_driver()
381
Peter Pentchev9c24be92022-09-26 22:35:24 +0300382+ def _setup_test_driver(self):
383+ """Initialize a StorPool driver as per the current configuration."""
384 mock_exec = mock.Mock()
385 mock_exec.return_value = ('', '')
386
Biser Milanov8323a8c2024-11-20 10:53:27 +0200387@@ -248,7 +546,7 @@ class StorPoolTestCase(test.TestCase):
388 self.driver.check_for_setup_error()
Peter Pentchev9c24be92022-09-26 22:35:24 +0300389
390 @ddt.data(
391- (5, TypeError),
392+ (5, (TypeError, AttributeError)),
393 ({'no-host': None}, KeyError),
394 ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
395 ({'host': 's01'}, None),
Biser Milanov8323a8c2024-11-20 10:53:27 +0200396@@ -264,7 +562,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300397 conn)
398
399 @ddt.data(
400- (5, TypeError),
401+ (5, (TypeError, AttributeError)),
402 ({'no-host': None}, KeyError),
403 ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
404 )
Biser Milanov8323a8c2024-11-20 10:53:27 +0200405@@ -317,7 +615,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev5a9f8a62023-12-06 10:40:18 +0200406 self.assertEqual(21, pool['total_capacity_gb'])
407 self.assertEqual(5, int(pool['free_capacity_gb']))
408
409- self.assertTrue(pool['multiattach'])
410+ self.assertFalse(pool['multiattach'])
411 self.assertFalse(pool['QoS_support'])
412 self.assertFalse(pool['thick_provisioning_support'])
413 self.assertTrue(pool['thin_provisioning_support'])
Biser Milanov8323a8c2024-11-20 10:53:27 +0200414@@ -735,6 +1033,139 @@ class StorPoolTestCase(test.TestCase):
415 'volume_type': volume_type
416 }))
417
Peter Pentchev9c24be92022-09-26 22:35:24 +0300418+
419+ @ddt.data(
420+ # The default values
Peter Pentchevea354462023-07-18 11:15:56 +0300421+ ('', False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300422+
423+ # Export to all
Peter Pentchevea354462023-07-18 11:15:56 +0300424+ ('*', True, constants.ISCSI, _ISCSI_IQN_OURS, True),
425+ ('*', True, constants.ISCSI, _ISCSI_IQN_OURS, True),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300426+
427+ # Only export to the controller
Peter Pentchevea354462023-07-18 11:15:56 +0300428+ ('', False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300429+
430+ # Some of the not-fully-supported pattern lists
Peter Pentchevea354462023-07-18 11:15:56 +0300431+ (_ISCSI_PAT_OTHER, False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
432+ (_ISCSI_PAT_OTHER, False, constants.STORPOOL, _ISCSI_IQN_OTHER, True),
433+ (_ISCSI_PAT_BOTH, False, constants.STORPOOL, _ISCSI_IQN_OURS, True),
434+ (_ISCSI_PAT_BOTH, False, constants.STORPOOL, _ISCSI_IQN_OTHER, True),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300435+ )
436+ @ddt.unpack
437+ def test_wants_iscsi(self, iscsi_export_to, use_iscsi, storage_protocol,
438+ hostname, expected):
439+ """Check the "should this export use iSCSI?" detection."""
440+ self.cfg.iscsi_export_to = iscsi_export_to
441+ self._setup_test_driver()
442+ self.assertEqual(self.driver._use_iscsi, use_iscsi)
443+
444+ # Make sure the driver reports the correct protocol in the stats
445+ self.driver._update_volume_stats()
446+ self.assertEqual(self.driver._stats["vendor_name"], "StorPool")
447+ self.assertEqual(self.driver._stats["storage_protocol"],
448+ storage_protocol)
449+
450+ def check(conn, forced, expected):
451+ """Pass partially or completely valid connector info."""
452+ for initiator in (None, hostname):
Peter Pentchevea354462023-07-18 11:15:56 +0300453+ for host in (None, _ISCSI_IQN_THIRD):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300454+ self.assertEqual(
455+ self.driver._connector_wants_iscsi({
456+ "host": host,
457+ "initiator": initiator,
458+ **conn,
459+ }),
460+ expected if initiator is not None and host is not None
461+ else forced)
462+
463+ # If iscsi_cinder_volume is set and this is the controller, then yes.
464+ check({"storpool_wants_iscsi": True}, True, True)
465+
466+ # If iscsi_cinder_volume is not set or this is not the controller, then
467+ # look at the specified expected value.
468+ check({"storpool_wants_iscsi": False}, use_iscsi, expected)
469+ check({}, use_iscsi, expected)
Peter Pentchevea354462023-07-18 11:15:56 +0300470+
471+ def _validate_iscsi_config(
472+ self,
Biser Milanov8323a8c2024-11-20 10:53:27 +0200473+ cfg: MockIscsiConfig,
Peter Pentchevea354462023-07-18 11:15:56 +0300474+ res: dict[str, Any],
475+ tcase: IscsiTestCase,
476+ ) -> None:
477+ """Make sure the returned structure makes sense."""
478+ initiator = res['initiator']
Biser Milanov8323a8c2024-11-20 10:53:27 +0200479+ cfg_initiator = cfg.initiators.get('1')
Peter Pentchevea354462023-07-18 11:15:56 +0300480+
Biser Milanov8323a8c2024-11-20 10:53:27 +0200481+ self.assertIs(res['cfg'].iscsi, cfg)
482+ self.assertEqual(res['pg'].name, _ISCSI_PORTAL_GROUP)
Peter Pentchevea354462023-07-18 11:15:56 +0300483+
484+ if tcase.initiator is None:
485+ self.assertIsNone(initiator)
486+ else:
487+ self.assertIsNotNone(initiator)
488+ self.assertEqual(initiator, cfg_initiator)
489+
490+ if tcase.volume is None:
491+ self.assertIsNone(res['target'])
492+ else:
493+ self.assertIsNotNone(res['target'])
Biser Milanov8323a8c2024-11-20 10:53:27 +0200494+ self.assertEqual(res['target'], cfg.targets.get('1'))
Peter Pentchevea354462023-07-18 11:15:56 +0300495+
496+ if tcase.initiator is None:
497+ self.assertIsNone(cfg_initiator)
498+ self.assertIsNone(res['export'])
499+ else:
500+ self.assertIsNotNone(cfg_initiator)
501+ if tcase.exported:
502+ self.assertIsNotNone(res['export'])
Biser Milanov8323a8c2024-11-20 10:53:27 +0200503+ self.assertEqual(res['export'], cfg_initiator.exports[0])
Peter Pentchevea354462023-07-18 11:15:56 +0300504+ else:
505+ self.assertIsNone(res['export'])
506+
507+ @ddt.data(*_ISCSI_TEST_CASES)
508+ def test_iscsi_get_config(self, tcase: IscsiTestCase) -> None:
509+ """Make sure the StorPool iSCSI configuration is parsed correctly."""
510+ cfg_orig = MockIscsiConfig.build(tcase)
511+ configs = [cfg_orig]
512+ iapi = MockIscsiAPI(configs, self)
Biser Milanov8323a8c2024-11-20 10:53:27 +0200513+ with mock.patch.object(self.driver._attach, 'api', new=lambda: iapi):
Peter Pentchevea354462023-07-18 11:15:56 +0300514+ res = self.driver._get_iscsi_config(
515+ _ISCSI_IQN_OURS,
516+ fconst.VOLUME_ID,
517+ )
518+
519+ self._validate_iscsi_config(cfg_orig, res, tcase)
520+
521+ @ddt.data(*_ISCSI_TEST_CASES)
522+ def test_iscsi_create_export(self, tcase: IscsiTestCase) -> None:
523+ """Make sure _create_iscsi_export() makes the right API calls."""
524+ cfg_orig = MockIscsiConfig.build(tcase)
525+ configs = [cfg_orig]
526+ iapi = MockIscsiAPI(configs, self)
Biser Milanov8323a8c2024-11-20 10:53:27 +0200527+ with mock.patch.object(self.driver._attach, 'api', new=lambda: iapi):
Peter Pentchevea354462023-07-18 11:15:56 +0300528+ self.driver._create_iscsi_export(
529+ {
530+ 'id': fconst.VOLUME_ID,
531+ 'display_name': fconst.VOLUME_NAME,
532+ },
533+ {
534+ # Yeah, okay, so we cheat a little bit here...
535+ 'host': _ISCSI_IQN_OURS + '.hostname',
536+ 'initiator': _ISCSI_IQN_OURS,
537+ },
538+ )
539+
540+ self.assertEqual(len(configs), tcase.commands_count + 1)
541+ cfg_final = configs[-1]
Biser Milanov8323a8c2024-11-20 10:53:27 +0200542+ self.assertEqual(cfg_final.initiators['1'].name, _ISCSI_IQN_OURS)
Peter Pentchevea354462023-07-18 11:15:56 +0300543+ self.assertEqual(
Biser Milanov8323a8c2024-11-20 10:53:27 +0200544+ cfg_final.initiators['1'].exports[0].target,
Peter Pentchevea354462023-07-18 11:15:56 +0300545+ targetName(fconst.VOLUME_ID),
546+ )
547+ self.assertEqual(
Biser Milanov8323a8c2024-11-20 10:53:27 +0200548+ cfg_final.targets['1'].volume,
Peter Pentchevea354462023-07-18 11:15:56 +0300549+ volumeName(fconst.VOLUME_ID),
550+ )
Biser Milanov8323a8c2024-11-20 10:53:27 +0200551 @mock_volume_types
552 def test_volume_revert(self):
553 vol_id = 'rev1'
Peter Pentchevacaaa382023-02-28 11:26:13 +0200554diff --git a/cinder/volume/drivers/storpool.py b/cinder/volume/drivers/storpool.py
Biser Milanov8323a8c2024-11-20 10:53:27 +0200555index a30e918cb..021e72322 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +0300556--- a/cinder/volume/drivers/storpool.py
557+++ b/cinder/volume/drivers/storpool.py
Biser Milanov8323a8c2024-11-20 10:53:27 +0200558@@ -15,6 +15,7 @@
Peter Pentchev9c24be92022-09-26 22:35:24 +0300559
Biser Milanov8323a8c2024-11-20 10:53:27 +0200560 """StorPool block device driver"""
561
Peter Pentchev9c24be92022-09-26 22:35:24 +0300562+import fnmatch
Biser Milanov8323a8c2024-11-20 10:53:27 +0200563 import platform
Biser Milanovc14fd452024-11-20 10:26:16 +0200564
Biser Milanov8323a8c2024-11-20 10:53:27 +0200565 from oslo_config import cfg
566@@ -43,6 +44,31 @@ if storpool:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300567
568
569 storpool_opts = [
570+ cfg.BoolOpt('iscsi_cinder_volume',
571+ default=False,
572+ help='Let the cinder-volume service use iSCSI instead of '
573+ 'the StorPool block device driver for accessing '
574+ 'StorPool volumes, e.g. when creating a volume from '
575+ 'an image or vice versa.'),
576+ cfg.StrOpt('iscsi_export_to',
577+ default='',
578+ help='Whether to export volumes using iSCSI. '
579+ 'An empty string (the default) makes the driver export '
580+ 'all volumes using the StorPool native network protocol. '
581+ 'The value "*" makes the driver export all volumes using '
582+ 'iSCSI. '
583+ 'Any other value leads to an experimental not fully '
584+ 'supported configuration and is interpreted as '
585+ 'a whitespace-separated list of patterns for IQNs for '
586+ 'hosts that need volumes to be exported via iSCSI, e.g. '
587+ '"iqn.1991-05.com.microsoft:\\*" for Windows hosts.'),
588+ cfg.BoolOpt('iscsi_learn_initiator_iqns',
589+ default=True,
590+ help='Create a StorPool record for a new initiator as soon as '
591+ 'Cinder asks for a volume to be exported to it.'),
592+ cfg.StrOpt('iscsi_portal_group',
593+ default=None,
594+ help='The portal group to export volumes via iSCSI in.'),
595 cfg.StrOpt('storpool_template',
596 default=None,
597 help='The StorPool template for volumes with no type.'),
Biser Milanov8323a8c2024-11-20 10:53:27 +0200598@@ -109,6 +135,7 @@ class StorPoolDriver(driver.VolumeDriver):
599 self._ourId = None
Peter Pentchev9c24be92022-09-26 22:35:24 +0300600 self._ourIdInt = None
Biser Milanov8323a8c2024-11-20 10:53:27 +0200601 self._attach = None
Peter Pentchev9c24be92022-09-26 22:35:24 +0300602+ self._use_iscsi = None
603
604 @staticmethod
605 def get_driver_options():
Biser Milanov8323a8c2024-11-20 10:53:27 +0200606@@ -177,10 +204,326 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300607 raise StorPoolConfigurationInvalid(
608 section=hostname, param='SP_OURID', error=e)
609
610+ def _connector_wants_iscsi(self, connector):
611+ """Should we do this export via iSCSI?
612+
613+ Check the configuration to determine whether this connector is
614+ expected to provide iSCSI exports as opposed to native StorPool
615+ protocol ones. Match the initiator's IQN against the list of
616+ patterns supplied in the "iscsi_export_to" configuration setting.
617+ """
618+ if connector is None:
619+ return False
620+ if self._use_iscsi:
621+ LOG.debug(' - forcing iSCSI for all exported volumes')
622+ return True
623+ if connector.get('storpool_wants_iscsi'):
624+ LOG.debug(' - forcing iSCSI for the controller')
625+ return True
626+
627+ try:
628+ iqn = connector.get('initiator')
629+ except Exception:
630+ iqn = None
631+ try:
632+ host = connector.get('host')
633+ except Exception:
634+ host = None
635+ if iqn is None or host is None:
636+ LOG.debug(' - this connector certainly does not want iSCSI')
637+ return False
638+
639+ LOG.debug(' - check whether %(host)s (%(iqn)s) wants iSCSI',
640+ {
641+ 'host': host,
642+ 'iqn': iqn,
643+ })
644+
645+ export_to = self.configuration.iscsi_export_to
646+ if export_to is None:
647+ return False
648+
649+ for pat in export_to.split():
650+ LOG.debug(' - matching against %(pat)s', {'pat': pat})
651+ if fnmatch.fnmatch(iqn, pat):
652+ LOG.debug(' - got it!')
653+ return True
654+ LOG.debug(' - nope')
655+ return False
656+
657 def validate_connector(self, connector):
658+ if self._connector_wants_iscsi(connector):
659+ return True
660 return self._storpool_client_id(connector) >= 0
661
662+ def _get_iscsi_config(self, iqn, volume_id):
663+ """Get the StorPool iSCSI config items pertaining to this volume.
664+
665+ Find the elements of the StorPool iSCSI configuration tree that
666+ will be needed to create, ensure, or remove the iSCSI export of
667+ the specified volume to the specified initiator.
668+ """
Biser Milanov8323a8c2024-11-20 10:53:27 +0200669+ cfg = self._attach.api().iSCSIConfig()
Peter Pentchev9c24be92022-09-26 22:35:24 +0300670+
671+ pg_name = self.configuration.iscsi_portal_group
672+ pg_found = [
Biser Milanov8323a8c2024-11-20 10:53:27 +0200673+ pg for pg in cfg.iscsi.portalGroups.values() if pg.name == pg_name
Peter Pentchev9c24be92022-09-26 22:35:24 +0300674+ ]
675+ if not pg_found:
676+ raise Exception('StorPool Cinder iSCSI configuration error: '
677+ 'no portal group "{pg}"'.format(pg=pg_name))
678+ pg = pg_found[0]
679+
680+ # Do we know about this initiator?
681+ i_found = [
Biser Milanov8323a8c2024-11-20 10:53:27 +0200682+ init for init in cfg.iscsi.initiators.values() if init.name == iqn
Peter Pentchev9c24be92022-09-26 22:35:24 +0300683+ ]
684+ if i_found:
685+ initiator = i_found[0]
686+ else:
687+ initiator = None
688+
689+ # Is this volume already being exported?
Biser Milanov8323a8c2024-11-20 10:53:27 +0200690+ volname = self._attach.volumeName(volume_id)
Peter Pentchev9c24be92022-09-26 22:35:24 +0300691+ t_found = [
Biser Milanov8323a8c2024-11-20 10:53:27 +0200692+ tgt for tgt in cfg.iscsi.targets.values() if tgt.volume == volname
Peter Pentchev9c24be92022-09-26 22:35:24 +0300693+ ]
694+ if t_found:
695+ target = t_found[0]
696+ else:
697+ target = None
698+
699+ # OK, so is this volume being exported to this initiator?
700+ export = None
701+ if initiator is not None and target is not None:
702+ e_found = [
Biser Milanov8323a8c2024-11-20 10:53:27 +0200703+ exp for exp in initiator.exports
704+ if exp.portalGroup == pg.name and exp.target == target.name
Peter Pentchev9c24be92022-09-26 22:35:24 +0300705+ ]
706+ if e_found:
707+ export = e_found[0]
708+
709+ return {
710+ 'cfg': cfg,
711+ 'pg': pg,
712+ 'initiator': initiator,
713+ 'target': target,
714+ 'export': export,
715+ 'volume_name': volname,
716+ 'volume_id': volume_id,
717+ }
718+
719+ def _create_iscsi_export(self, volume, connector):
720+ """Create (if needed) an iSCSI export for the StorPool volume."""
721+ LOG.debug(
722+ '_create_iscsi_export() invoked for volume '
723+ '"%(vol_name)s" (%(vol_id)s) connector %(connector)s',
724+ {
725+ 'vol_name': volume['display_name'],
726+ 'vol_id': volume['id'],
727+ 'connector': connector,
728+ }
729+ )
730+ iqn = connector['initiator']
731+ try:
732+ cfg = self._get_iscsi_config(iqn, volume['id'])
733+ except Exception as exc:
734+ LOG.error(
735+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
736+ )
737+ raise
738+
739+ if cfg['initiator'] is None:
740+ if not (self.configuration.iscsi_learn_initiator_iqns or
741+ self.configuration.iscsi_cinder_volume and
742+ connector.get('storpool_wants_iscsi')):
743+ raise Exception('The "{iqn}" initiator IQN for the "{host}" '
744+ 'host is not defined in the StorPool '
745+ 'configuration.'
746+ .format(iqn=iqn, host=connector['host']))
747+ else:
748+ LOG.info('Creating a StorPool iSCSI initiator '
749+ 'for "{host}s" ({iqn}s)',
750+ {'host': connector['host'], 'iqn': iqn})
Biser Milanov8323a8c2024-11-20 10:53:27 +0200751+ self._attach.api().iSCSIConfigChange({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300752+ 'commands': [
753+ {
754+ 'createInitiator': {
755+ 'name': iqn,
756+ 'username': '',
757+ 'secret': '',
758+ },
759+ },
760+ {
761+ 'initiatorAddNetwork': {
762+ 'initiator': iqn,
763+ 'net': '0.0.0.0/0',
764+ },
765+ },
766+ ]
767+ })
768+
769+ if cfg['target'] is None:
770+ LOG.info(
771+ 'Creating a StorPool iSCSI target '
772+ 'for the "%(vol_name)s" volume (%(vol_id)s)',
773+ {
774+ 'vol_name': volume['display_name'],
775+ 'vol_id': volume['id'],
776+ }
777+ )
Biser Milanov8323a8c2024-11-20 10:53:27 +0200778+ self._attach.api().iSCSIConfigChange({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300779+ 'commands': [
780+ {
781+ 'createTarget': {
782+ 'volumeName': cfg['volume_name'],
783+ },
784+ },
785+ ]
786+ })
787+ cfg = self._get_iscsi_config(iqn, volume['id'])
788+
789+ if cfg['export'] is None:
790+ LOG.info('Creating a StorPool iSCSI export '
791+ 'for the "{vol_name}s" volume ({vol_id}s) '
792+ 'to the "{host}s" initiator ({iqn}s) '
793+ 'in the "{pg}s" portal group',
794+ {
795+ 'vol_name': volume['display_name'],
796+ 'vol_id': volume['id'],
797+ 'host': connector['host'],
798+ 'iqn': iqn,
Biser Milanov8323a8c2024-11-20 10:53:27 +0200799+ 'pg': cfg['pg'].name
Peter Pentchev9c24be92022-09-26 22:35:24 +0300800+ })
Biser Milanov8323a8c2024-11-20 10:53:27 +0200801+ self._attach.api().iSCSIConfigChange({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300802+ 'commands': [
803+ {
804+ 'export': {
805+ 'initiator': iqn,
Biser Milanov8323a8c2024-11-20 10:53:27 +0200806+ 'portalGroup': cfg['pg'].name,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300807+ 'volumeName': cfg['volume_name'],
808+ },
809+ },
810+ ]
811+ })
812+
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200813+ target_portals = [
Biser Milanov8323a8c2024-11-20 10:53:27 +0200814+ "{addr}:3260".format(addr=net.address)
815+ for net in cfg['pg'].networks
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200816+ ]
Biser Milanov8323a8c2024-11-20 10:53:27 +0200817+ target_iqns = [cfg['target'].name] * len(target_portals)
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200818+ target_luns = [0] * len(target_portals)
819+ if connector.get('multipath', False):
820+ multipath_settings = {
821+ 'target_iqns': target_iqns,
822+ 'target_portals': target_portals,
823+ 'target_luns': target_luns,
824+ }
825+ else:
826+ multipath_settings = {}
827+
Peter Pentchev9c24be92022-09-26 22:35:24 +0300828+ res = {
829+ 'driver_volume_type': 'iscsi',
830+ 'data': {
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200831+ **multipath_settings,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300832+ 'target_discovered': False,
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200833+ 'target_iqn': target_iqns[0],
834+ 'target_portal': target_portals[0],
835+ 'target_lun': target_luns[0],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300836+ 'volume_id': volume['id'],
837+ 'discard': True,
838+ },
839+ }
840+ LOG.debug('returning %(res)s', {'res': res})
841+ return res
842+
843+ def _remove_iscsi_export(self, volume, connector):
844+ """Remove an iSCSI export for the specified StorPool volume."""
845+ LOG.debug(
846+ '_remove_iscsi_export() invoked for volume '
847+ '"%(vol_name)s" (%(vol_id)s) connector %(conn)s',
848+ {
849+ 'vol_name': volume['display_name'],
850+ 'vol_id': volume['id'],
851+ 'conn': connector,
852+ }
853+ )
854+ try:
855+ cfg = self._get_iscsi_config(connector['initiator'], volume['id'])
856+ except Exception as exc:
857+ LOG.error(
858+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
859+ )
860+ raise
861+
862+ if cfg['export'] is not None:
863+ LOG.info('Removing the StorPool iSCSI export '
864+ 'for the "%(vol_name)s" volume (%(vol_id)s) '
865+ 'to the "%(host)s" initiator (%(iqn)s) '
866+ 'in the "%(pg)s" portal group',
867+ {
868+ 'vol_name': volume['display_name'],
869+ 'vol_id': volume['id'],
870+ 'host': connector['host'],
871+ 'iqn': connector['initiator'],
Biser Milanov8323a8c2024-11-20 10:53:27 +0200872+ 'pg': cfg['pg'].name,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300873+ })
874+ try:
Biser Milanov8323a8c2024-11-20 10:53:27 +0200875+ self._attach.api().iSCSIConfigChange({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300876+ 'commands': [
877+ {
878+ 'exportDelete': {
Biser Milanov8323a8c2024-11-20 10:53:27 +0200879+ 'initiator': cfg['initiator'].name,
880+ 'portalGroup': cfg['pg'].name,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300881+ 'volumeName': cfg['volume_name'],
882+ },
883+ },
884+ ]
885+ })
Biser Milanov8323a8c2024-11-20 10:53:27 +0200886+ except spapi.ApiError as e:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300887+ if e.name not in ('objectExists', 'objectDoesNotExist'):
888+ raise
889+ LOG.info('Looks like somebody beat us to it')
890+
891+ if cfg['target'] is not None:
892+ last = True
Biser Milanov8323a8c2024-11-20 10:53:27 +0200893+ for initiator in cfg['cfg'].iscsi.initiators.values():
894+ if initiator.name == cfg['initiator'].name:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300895+ continue
Biser Milanov8323a8c2024-11-20 10:53:27 +0200896+ for exp in initiator.exports:
897+ if exp.target == cfg['target'].name:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300898+ last = False
899+ break
900+ if not last:
901+ break
902+
903+ if last:
904+ LOG.info(
905+ 'Removing the StorPool iSCSI target '
906+ 'for the "{vol_name}s" volume ({vol_id}s)',
907+ {
908+ 'vol_name': volume['display_name'],
909+ 'vol_id': volume['id'],
910+ }
911+ )
912+ try:
Biser Milanov8323a8c2024-11-20 10:53:27 +0200913+ self._attach.api().iSCSIConfigChange({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300914+ 'commands': [
915+ {
916+ 'deleteTarget': {
917+ 'volumeName': cfg['volume_name'],
918+ },
919+ },
920+ ]
921+ })
Biser Milanov8323a8c2024-11-20 10:53:27 +0200922+ except spapi.ApiError as e:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300923+ if e.name not in ('objectDoesNotExist', 'invalidParam'):
924+ raise
925+ LOG.info('Looks like somebody beat us to it')
926+
927 def initialize_connection(self, volume, connector):
928+ if self._connector_wants_iscsi(connector):
929+ return self._create_iscsi_export(volume, connector)
930 return {'driver_volume_type': 'storpool',
931 'data': {
932 'client_id': self._storpool_client_id(connector),
Biser Milanov8323a8c2024-11-20 10:53:27 +0200933@@ -189,6 +532,9 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300934 }}
935
936 def terminate_connection(self, volume, connector, **kwargs):
937+ if self._connector_wants_iscsi(connector):
938+ LOG.debug('- removing an iSCSI export')
939+ self._remove_iscsi_export(volume, connector)
940 pass
941
942 def create_snapshot(self, snapshot):
Biser Milanov8323a8c2024-11-20 10:53:27 +0200943@@ -293,11 +639,20 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300944 )
945
946 def create_export(self, context, volume, connector):
947- pass
948+ if self._connector_wants_iscsi(connector):
949+ LOG.debug('- creating an iSCSI export')
950+ self._create_iscsi_export(volume, connector)
951
952 def remove_export(self, context, volume):
953 pass
954
955+ def _attach_volume(self, context, volume, properties, remote=False):
956+ if self.configuration.iscsi_cinder_volume and not remote:
957+ LOG.debug('- adding the "storpool_wants_iscsi" flag')
958+ properties['storpool_wants_iscsi'] = True
959+
960+ return super()._attach_volume(context, volume, properties, remote)
961+
962 def delete_volume(self, volume):
Biser Milanov8323a8c2024-11-20 10:53:27 +0200963 name = self._attach.volumeName(volume['id'])
Peter Pentchev9c24be92022-09-26 22:35:24 +0300964 try:
Biser Milanov8323a8c2024-11-20 10:53:27 +0200965@@ -334,6 +689,17 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300966 LOG.error("StorPoolDriver API initialization failed: %s", e)
967 raise
968
969+ export_to = self.configuration.iscsi_export_to
970+ export_to_set = export_to is not None and export_to.split()
971+ vol_iscsi = self.configuration.iscsi_cinder_volume
972+ pg_name = self.configuration.iscsi_portal_group
973+ if (export_to_set or vol_iscsi) and pg_name is None:
974+ msg = _('The "iscsi_portal_group" option is required if '
975+ 'any patterns are listed in "iscsi_export_to"')
976+ raise exception.VolumeDriverException(message=msg)
977+
978+ self._use_iscsi = export_to == "*"
979+
980 def _update_volume_stats(self):
981 try:
Biser Milanov8323a8c2024-11-20 10:53:27 +0200982 dl = self._attach.api().disksList()
983@@ -359,7 +725,7 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300984 'total_capacity_gb': total / units.Gi,
985 'free_capacity_gb': free / units.Gi,
986 'reserved_percentage': 0,
987- 'multiattach': True,
Peter Pentchev5a9f8a62023-12-06 10:40:18 +0200988+ 'multiattach': self._use_iscsi,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300989 'QoS_support': False,
990 'thick_provisioning_support': False,
991 'thin_provisioning_support': True,
Biser Milanov8323a8c2024-11-20 10:53:27 +0200992@@ -378,7 +744,9 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300993 'volume_backend_name') or 'storpool',
994 'vendor_name': 'StorPool',
995 'driver_version': self.VERSION,
996- 'storage_protocol': constants.STORPOOL,
997+ 'storage_protocol': (
998+ constants.ISCSI if self._use_iscsi else constants.STORPOOL
999+ ),
Peter Pentchevacaaa382023-02-28 11:26:13 +02001000 # Driver capabilities
Peter Pentchev9c24be92022-09-26 22:35:24 +03001001 'clone_across_pools': True,
1002 'sparse_copy_volume': True,
Peter Pentchevacaaa382023-02-28 11:26:13 +02001003diff --git a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
Biser Milanov8323a8c2024-11-20 10:53:27 +02001004index d2c5895a9..1f3d46cce 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +03001005--- a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
1006+++ b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
Peter Pentchevacaaa382023-02-28 11:26:13 +02001007@@ -19,12 +19,15 @@ Prerequisites
Peter Pentchev9c24be92022-09-26 22:35:24 +03001008 * The controller and all the compute nodes must have access to the StorPool
1009 API service.
1010
1011-* All nodes where StorPool-backed volumes will be attached must have access to
1012+* If iSCSI is not being used as a transport (see below), all nodes where
1013+ StorPool-backed volumes will be attached must have access to
1014 the StorPool data network and run the ``storpool_block`` service.
1015
1016-* If StorPool-backed Cinder volumes need to be created directly from Glance
1017- images, then the node running the ``cinder-volume`` service must also have
1018- access to the StorPool data network and run the ``storpool_block`` service.
1019+* If Glance uses Cinder as its image store, or if StorPool-backed Cinder
1020+ volumes need to be created directly from Glance images, and iSCSI is not
1021+ being used as a transport, then the node running the ``cinder-volume``
1022+ service must also have access to the StorPool data network and run
1023+ the ``storpool_block`` service.
1024
1025 * All nodes that need to access the StorPool API (the compute nodes and
1026 the node running the ``cinder-volume`` service) must have the following
Biser Milanov8323a8c2024-11-20 10:53:27 +02001027@@ -34,6 +37,29 @@ Prerequisites
Peter Pentchev9c24be92022-09-26 22:35:24 +03001028 * the storpool Python bindings package
1029 * the storpool.spopenstack Python helper package
1030
1031+Using iSCSI as the transport protocol
1032+-------------------------------------
1033+
1034+The StorPool distributed storage system uses its own, highly optimized and
1035+tailored for its specifics, network protocol for communication between
1036+the storage servers and the clients (the OpenStack cluster nodes where
1037+StorPool-backed volumes will be attached). There are cases when granting
1038+various nodes access to the StorPool data network or installing and
1039+running the ``storpool_block`` client service on them may pose difficulties.
1040+The StorPool servers may also expose the user-created volumes and snapshots
1041+using the standard iSCSI protocol that only requires TCP routing and
1042+connectivity between the storage servers and the StorPool clients.
1043+The StorPool Cinder driver may be configured to export volumes and
1044+snapshots via iSCSI using the ``iscsi_export_to`` and ``iscsi_portal_group``
1045+configuration options.
1046+
1047+Additionally, even if e.g. the hypervisor nodes running Nova will use
1048+the StorPool network protocol and run the ``storpool_block`` service
1049+(so the ``iscsi_export_to`` option has its default empty string value),
1050+the ``iscsi_cinder_volume`` option configures the StorPool Cinder driver
1051+so that only the ``cinder-volume`` service will use the iSCSI protocol when
1052+attaching volumes and snapshots to transfer data to and from Glance images.
1053+
1054 Configuring the StorPool volume driver
1055 --------------------------------------
1056
Biser Milanov8323a8c2024-11-20 10:53:27 +02001057@@ -55,6 +81,32 @@ volume backend definition) and per volume type:
Peter Pentchev9c24be92022-09-26 22:35:24 +03001058 with the default placement constraints for the StorPool cluster.
1059 The default value for the chain replication is 3.
1060
Peter Pentchevea354462023-07-18 11:15:56 +03001061+In addition, if the iSCSI protocol is used to access the StorPool cluster as
1062+described in the previous section, the following options may be defined in
1063+the ``cinder.conf`` volume backend definition:
1064+
Peter Pentchev9c24be92022-09-26 22:35:24 +03001065+- ``iscsi_export_to``: if set to the value ``*``, the StorPool Cinder driver
1066+ will export volumes and snapshots using the iSCSI protocol instead of
1067+ the StorPool network protocol. The ``iscsi_portal_group`` option must also
1068+ be specified.
1069+
1070+- ``iscsi_portal_group``: if the ``iscsi_export_to`` option is set to
1071+ the value ``*`` or the ``iscsi_cinder_volume`` option is turned on,
1072+ this option specifies the name of the iSCSI portal group that Cinder
1073+ volumes will be exported to.
1074+
1075+- ``iscsi_cinder_volume``: if enabled, even if the ``iscsi_export_to`` option
1076+ has its default empty value, the ``cinder-volume`` service will use iSCSI
1077+ to attach the volumes and snapshots for transferring data to and from
Biser Milanov8323a8c2024-11-20 10:53:27 +02001078+ Glance images.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001079+
Peter Pentchevea354462023-07-18 11:15:56 +03001080+- ``iscsi_learn_initiator_iqns``: if enabled, the StorPool Cinder driver will
1081+ automatically use the StorPool API to create definitions for new initiators
1082+ in the StorPool cluster's configuration. This is the default behavior of
1083+ the driver; it may be disabled in the rare case if, e.g. because of site
1084+ policy, OpenStack iSCSI initiators (e.g. Nova hypervisors) need to be
1085+ explicitly allowed to use the StorPool iSCSI targets.
1086+
Peter Pentchev9c24be92022-09-26 22:35:24 +03001087 Using the StorPool volume driver
1088 --------------------------------
1089
Biser Milanov8323a8c2024-11-20 10:53:27 +02001090diff --git a/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml b/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
1091new file mode 100644
1092index 000000000..edf46d298
1093--- /dev/null
1094+++ b/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
1095@@ -0,0 +1,13 @@
1096+---
1097+features:
1098+ - |
1099+ StorPool driver: Added support for exporting the StorPool-backed volumes
1100+ using the iSCSI protocol, so that the Cinder volume service and/or
1101+ the Nova or Glance consumers do not need to have the StorPool block
1102+ device third-party service installed. See the StorPool driver section in
1103+ the Cinder documentation for more information on the ``iscsi_export_to``,
1104+ ``iscsi_portal_group``, ``iscsi_cinder_volume``, and
1105+ ``iscsi_learn_initiator_iqns`` options.
1106+ Note that multiattach support for StorPool is now only enabled if
1107+ ``iscsi_export_to`` is set to ``*`, that is, all StorPool volumes will be
1108+ exported via iSCSI to all initiators.
Peter Pentchevacaaa382023-02-28 11:26:13 +02001109--
Biser Milanovc14fd452024-11-20 10:26:16 +020011102.43.0
Peter Pentchevacaaa382023-02-28 11:26:13 +02001111