blob: 1ec090804e487b47ab547215230efed04afb19ed [file] [log] [blame]
Biser Milanovec3bf982024-11-27 17:42:17 +02001From 2acfdddf0794754aa0e32a56800e77387f75ce38 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 Milanovda1b0682024-11-29 09:37:10 +020028 .../unit/volume/drivers/test_storpool.py | 521 +++++++++++++++++-
29 cinder/volume/drivers/storpool.py | 379 ++++++++++++-
30 .../drivers/storpool-volume-driver.rst | 68 ++-
31 .../storpool-iscsi-cefcfe590a07c5c7.yaml | 15 +
32 4 files changed, 972 insertions(+), 11 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 Milanovda1b0682024-11-29 09:37:10 +020036index 44707d0b8..d6347e1d5 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 Milanovda1b0682024-11-29 09:37:10 +020039@@ -14,14 +14,24 @@
40 # under the License.
Peter Pentchevea354462023-07-18 11:15:56 +030041
42
Biser Milanovda1b0682024-11-29 09:37:10 +020043+from __future__ import annotations
44+
Peter Pentchevea354462023-07-18 11:15:56 +030045+import dataclasses
46 import itertools
47 import re
Biser Milanovda1b0682024-11-29 09:37:10 +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 Milanovda1b0682024-11-29 09:37:10 +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,6 +41,7 @@ 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
Biser Milanovec3bf982024-11-27 17:42:17 +020070 from cinder.tests.unit import fake_constants
Peter Pentchev9c24be92022-09-26 22:35:24 +030071 from cinder.tests.unit import test
Biser Milanovda1b0682024-11-29 09:37:10 +020072@@ -38,6 +49,13 @@ from cinder.volume import configuration as conf
73 from cinder.volume.drivers import storpool as driver
74
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 Milanovda1b0682024-11-29 09:37:10 +020082+
Peter Pentchevea354462023-07-18 11:15:56 +030083 volume_types = {
Biser Milanovec3bf982024-11-27 17:42:17 +020084 fake_constants.VOLUME_TYPE_ID: {},
Biser Milanovda1b0682024-11-29 09:37:10 +020085 fake_constants.VOLUME_TYPE2_ID: {'storpool_template': 'ssd'},
86@@ -71,6 +89,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 Milanovda1b0682024-11-29 09:37:10 +020094 class MockDisk(object):
95 def __init__(self, diskId):
96 self.id = diskId
97@@ -195,6 +217,315 @@ def MockVolumeUpdateDesc(size):
98 return {'size': size}
Peter Pentchevea354462023-07-18 11:15:56 +030099
Biser Milanovda1b0682024-11-29 09:37:10 +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 Milanovda1b0682024-11-29 09:37:10 +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 Milanovda1b0682024-11-29 09:37:10 +0200158+ def build(cls, tcase: IscsiTestCase) -> Self:
Peter Pentchevea354462023-07-18 11:15:56 +0300159+ """Build a test config structure."""
160+ initiators = {
Biser Milanovda1b0682024-11-29 09:37:10 +0200161+ '0': MockIscsiInitiator(name=_ISCSI_IQN_OTHER, exports=[]),
Peter Pentchevea354462023-07-18 11:15:56 +0300162+ }
163+ if tcase.initiator is not None:
Biser Milanovda1b0682024-11-29 09:37:10 +0200164+ initiators['1'] = MockIscsiInitiator(
165+ name=tcase.initiator,
166+ exports=(
Peter Pentchevea354462023-07-18 11:15:56 +0300167+ [
Biser Milanovda1b0682024-11-29 09:37:10 +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 Milanovda1b0682024-11-29 09:37:10 +0200176+ )
Peter Pentchevea354462023-07-18 11:15:56 +0300177+
178+ targets = {
Biser Milanovda1b0682024-11-29 09:37:10 +0200179+ '0': MockIscsiTarget(
180+ name=targetName(fake_constants.VOLUME2_ID),
181+ volume=volumeName(fake_constants.VOLUME2_ID),
182+ ),
Peter Pentchevea354462023-07-18 11:15:56 +0300183+ }
184+ if tcase.volume is not None:
Biser Milanovda1b0682024-11-29 09:37:10 +0200185+ targets['1'] = MockIscsiTarget(
186+ name=targetName(tcase.volume),
187+ volume=volumeName(tcase.volume),
188+ )
Peter Pentchevea354462023-07-18 11:15:56 +0300189+
Biser Milanovda1b0682024-11-29 09:37:10 +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 Milanovda1b0682024-11-29 09:37:10 +0200202+ ),
Peter Pentchevea354462023-07-18 11:15:56 +0300203+ },
Biser Milanovda1b0682024-11-29 09:37:10 +0200204+ initiators=initiators,
205+ targets=targets,
206+ )
Peter Pentchevea354462023-07-18 11:15:56 +0300207+
208+
Biser Milanovda1b0682024-11-29 09:37:10 +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 Milanovda1b0682024-11-29 09:37:10 +0200220+ _configs: list[MockIscsiConfig]
Peter Pentchevea354462023-07-18 11:15:56 +0300221+
222+ def __init__(
223+ self,
Biser Milanovda1b0682024-11-29 09:37:10 +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 Milanovda1b0682024-11-29 09:37:10 +0200231+ def iSCSIConfig(self) -> MockIscsiConfigTop:
Peter Pentchevea354462023-07-18 11:15:56 +0300232+ """Return the last version of the iSCSI configuration."""
Biser Milanovda1b0682024-11-29 09:37:10 +0200233+ return MockIscsiConfigTop(iscsi=self._configs[-1])
Peter Pentchevea354462023-07-18 11:15:56 +0300234+
Biser Milanovda1b0682024-11-29 09:37:10 +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,
Biser Milanovec3bf982024-11-27 17:42:17 +0200245+ 'volumeName': volumeName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300246+ },
247+ )
Biser Milanovda1b0682024-11-29 09:37:10 +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 Milanovda1b0682024-11-29 09:37:10 +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(fake_constants.VOLUME_ID),
261+ ),
262+ ],
263+ ),
Peter Pentchevea354462023-07-18 11:15:56 +0300264+ },
Biser Milanovda1b0682024-11-29 09:37:10 +0200265+ )
Peter Pentchevea354462023-07-18 11:15:56 +0300266+
Biser Milanovda1b0682024-11-29 09:37:10 +0200267+ def _handle_delete_export(
268+ self,
269+ cfg: MockIscsiConfig,
270+ cmd: dict[str, Any],
271+ ) -> MockIscsiConfig:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200272+ """Delete an export for an initiator."""
273+ self._asrt.assertDictEqual(
274+ cmd,
275+ {
276+ 'initiator': _ISCSI_IQN_OURS,
277+ 'portalGroup': _ISCSI_PORTAL_GROUP,
Biser Milanovec3bf982024-11-27 17:42:17 +0200278+ 'volumeName': volumeName(fake_constants.VOLUME_ID),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200279+ },
280+ )
Biser Milanovda1b0682024-11-29 09:37:10 +0200281+ self._asrt.assertEqual(cfg.initiators['1'].name, cmd['initiator'])
Biser Milanovd684c1c2024-11-22 12:12:59 +0200282+ self._asrt.assertListEqual(
Biser Milanovda1b0682024-11-29 09:37:10 +0200283+ cfg.initiators['1'].exports,
284+ [MockIscsiExport(portalGroup=_ISCSI_PORTAL_GROUP,
285+ target=cfg.targets['1'].name)])
Biser Milanovd684c1c2024-11-22 12:12:59 +0200286+
Biser Milanovda1b0682024-11-29 09:37:10 +0200287+ updated_initiators = cfg.initiators
288+ del updated_initiators['1']
289+ return dataclasses.replace(cfg, initiators=updated_initiators)
Biser Milanovd684c1c2024-11-22 12:12:59 +0200290+
Biser Milanovda1b0682024-11-29 09:37:10 +0200291+ def _handle_create_initiator(
292+ self,
293+ cfg: MockIscsiConfig,
294+ cmd: dict[str, Any],
295+ ) -> MockIscsiConfig:
Peter Pentchevea354462023-07-18 11:15:56 +0300296+ """Add a whole new initiator."""
297+ self._asrt.assertDictEqual(
298+ cmd,
299+ {
300+ 'name': _ISCSI_IQN_OURS,
301+ 'username': '',
302+ 'secret': '',
303+ },
304+ )
305+ self._asrt.assertNotIn(
306+ cmd['name'],
Biser Milanovda1b0682024-11-29 09:37:10 +0200307+ [init.name for init in cfg.initiators.values()],
Peter Pentchevea354462023-07-18 11:15:56 +0300308+ )
Biser Milanovda1b0682024-11-29 09:37:10 +0200309+ self._asrt.assertListEqual(sorted(cfg.initiators), ['0'])
Peter Pentchevea354462023-07-18 11:15:56 +0300310+
Biser Milanovda1b0682024-11-29 09:37:10 +0200311+ return dataclasses.replace(
312+ cfg,
313+ initiators={
314+ **cfg.initiators,
315+ '1': MockIscsiInitiator(name=cmd['name'], exports=[]),
316+ },
317+ )
Peter Pentchevea354462023-07-18 11:15:56 +0300318+
Biser Milanovda1b0682024-11-29 09:37:10 +0200319+ def _handle_create_target(
320+ self,
321+ cfg: MockIscsiConfig,
322+ cmd: dict[str, Any],
323+ ) -> MockIscsiConfig:
Peter Pentchevea354462023-07-18 11:15:56 +0300324+ """Add a target for a volume so that it may be exported."""
325+ self._asrt.assertDictEqual(
326+ cmd,
Biser Milanovec3bf982024-11-27 17:42:17 +0200327+ {'volumeName': volumeName(fake_constants.VOLUME_ID)},
Peter Pentchevea354462023-07-18 11:15:56 +0300328+ )
Biser Milanovda1b0682024-11-29 09:37:10 +0200329+ self._asrt.assertListEqual(sorted(cfg.targets), ['0'])
330+ return dataclasses.replace(
331+ cfg,
332+ targets={
333+ **cfg.targets,
334+ '1': MockIscsiTarget(
335+ name=targetName(fake_constants.VOLUME_ID),
336+ volume=volumeName(fake_constants.VOLUME_ID),
337+ ),
Peter Pentchevea354462023-07-18 11:15:56 +0300338+ },
Biser Milanovda1b0682024-11-29 09:37:10 +0200339+ )
Peter Pentchevea354462023-07-18 11:15:56 +0300340+
Biser Milanovda1b0682024-11-29 09:37:10 +0200341+ def _handle_delete_target(
342+ self,
343+ cfg: MockIscsiConfig,
344+ cmd: dict[str, Any]
345+ ) -> MockIscsiConfig:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200346+ """Remove a target for a volume."""
347+ self._asrt.assertDictEqual(
348+ cmd,
Biser Milanovec3bf982024-11-27 17:42:17 +0200349+ {'volumeName': volumeName(fake_constants.VOLUME_ID)},
Biser Milanovd684c1c2024-11-22 12:12:59 +0200350+ )
351+
Biser Milanovda1b0682024-11-29 09:37:10 +0200352+ self._asrt.assertListEqual(sorted(cfg.targets), ['0', '1'])
353+ updated_targets = cfg.targets
354+ del updated_targets['1']
355+ return dataclasses.replace(cfg, targets=updated_targets)
Biser Milanovd684c1c2024-11-22 12:12:59 +0200356+
Peter Pentchevea354462023-07-18 11:15:56 +0300357+ def _handle_initiator_add_network(
358+ self,
Biser Milanovda1b0682024-11-29 09:37:10 +0200359+ cfg: MockIscsiConfig,
Peter Pentchevea354462023-07-18 11:15:56 +0300360+ cmd: dict[str, Any],
Biser Milanovda1b0682024-11-29 09:37:10 +0200361+ ) -> MockIscsiConfig:
Peter Pentchevea354462023-07-18 11:15:56 +0300362+ """Add a network that an initiator is allowed to log in from."""
363+ self._asrt.assertDictEqual(
364+ cmd,
365+ {
366+ 'initiator': _ISCSI_IQN_OURS,
367+ 'net': '0.0.0.0/0',
368+ },
369+ )
Biser Milanovda1b0682024-11-29 09:37:10 +0200370+ return dataclasses.replace(cfg)
Peter Pentchevea354462023-07-18 11:15:56 +0300371+
372+ _CMD_HANDLERS = {
373+ 'createInitiator': _handle_create_initiator,
374+ 'createTarget': _handle_create_target,
Biser Milanovd684c1c2024-11-22 12:12:59 +0200375+ 'deleteTarget': _handle_delete_target,
Peter Pentchevea354462023-07-18 11:15:56 +0300376+ 'export': _handle_export,
Biser Milanovd684c1c2024-11-22 12:12:59 +0200377+ 'exportDelete': _handle_delete_export,
Peter Pentchevea354462023-07-18 11:15:56 +0300378+ 'initiatorAddNetwork': _handle_initiator_add_network,
379+ }
380+
Biser Milanovda1b0682024-11-29 09:37:10 +0200381+ def iSCSIConfigChange(
Peter Pentchevea354462023-07-18 11:15:56 +0300382+ self,
383+ commands: dict[str, list[dict[str, dict[str, Any]]]],
384+ ) -> None:
385+ """Apply the requested changes to the iSCSI configuration.
386+
387+ This method adds a new config object to the configs list,
388+ making a shallow copy of the last one and applying the changes
389+ specified in the list of commands.
390+ """
391+ self._asrt.assertListEqual(sorted(commands), ['commands'])
392+ self._asrt.assertGreater(len(commands['commands']), 0)
393+ for cmd in commands['commands']:
394+ keys = sorted(cmd.keys())
395+ cmd_name = keys[0]
396+ self._asrt.assertListEqual(keys, [cmd_name])
397+ handler = self._CMD_HANDLERS[cmd_name]
398+ new_cfg = handler(self, self._configs[-1], cmd[cmd_name])
399+ self._configs.append(new_cfg)
400+
401+
402+_ISCSI_TEST_CASES = [
403+ IscsiTestCase(None, None, False, 4),
404+ IscsiTestCase(_ISCSI_IQN_OURS, None, False, 2),
Biser Milanovec3bf982024-11-27 17:42:17 +0200405+ IscsiTestCase(_ISCSI_IQN_OURS, fake_constants.VOLUME_ID, False, 1),
406+ IscsiTestCase(_ISCSI_IQN_OURS, fake_constants.VOLUME_ID, True, 0),
Peter Pentchevea354462023-07-18 11:15:56 +0300407+]
408+
Biser Milanovda1b0682024-11-29 09:37:10 +0200409+
Peter Pentchevea354462023-07-18 11:15:56 +0300410 def MockSPConfig(section = 's01'):
411 res = {}
Biser Milanovda1b0682024-11-29 09:37:10 +0200412 m = re.match('^s0*([A-Za-z0-9]+)$', section)
413@@ -237,7 +568,15 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300414 self.cfg.volume_backend_name = 'storpool_test'
415 self.cfg.storpool_template = None
416 self.cfg.storpool_replication = 3
Biser Milanovd684c1c2024-11-22 12:12:59 +0200417+ self.cfg.storpool_iscsi_cinder_volume = False
418+ self.cfg.storpool_iscsi_export_to = ''
419+ self.cfg.storpool_iscsi_learn_initiator_iqns = True
420+ self.cfg.storpool_iscsi_portal_group = _ISCSI_PORTAL_GROUP
Peter Pentchevea354462023-07-18 11:15:56 +0300421
Biser Milanovd684c1c2024-11-22 12:12:59 +0200422+ self._setup_test_driver()
423+
Peter Pentchev9c24be92022-09-26 22:35:24 +0300424+ def _setup_test_driver(self):
425+ """Initialize a StorPool driver as per the current configuration."""
426 mock_exec = mock.Mock()
427 mock_exec.return_value = ('', '')
428
Biser Milanovda1b0682024-11-29 09:37:10 +0200429@@ -246,7 +585,7 @@ class StorPoolTestCase(test.TestCase):
430 self.driver.check_for_setup_error()
Peter Pentchev9c24be92022-09-26 22:35:24 +0300431
432 @ddt.data(
433- (5, TypeError),
434+ (5, (TypeError, AttributeError)),
435 ({'no-host': None}, KeyError),
436 ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
437 ({'host': 's01'}, None),
Biser Milanovda1b0682024-11-29 09:37:10 +0200438@@ -262,7 +601,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300439 conn)
440
441 @ddt.data(
442- (5, TypeError),
443+ (5, (TypeError, AttributeError)),
444 ({'no-host': None}, KeyError),
445 ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
446 )
Biser Milanovda1b0682024-11-29 09:37:10 +0200447@@ -301,7 +640,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev5a9f8a62023-12-06 10:40:18 +0200448 self.assertEqual(21, pool['total_capacity_gb'])
449 self.assertEqual(5, int(pool['free_capacity_gb']))
450
451- self.assertTrue(pool['multiattach'])
452+ self.assertFalse(pool['multiattach'])
453 self.assertFalse(pool['QoS_support'])
454 self.assertFalse(pool['thick_provisioning_support'])
455 self.assertTrue(pool['thin_provisioning_support'])
Biser Milanovda1b0682024-11-29 09:37:10 +0200456@@ -720,3 +1059,179 @@ class StorPoolTestCase(test.TestCase):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200457 'No such volume',
458 self.driver.revert_to_snapshot, None,
459 {'id': vol_id}, {'id': snap_id})
Peter Pentchev9c24be92022-09-26 22:35:24 +0300460+
461+ @ddt.data(
462+ # The default values
Peter Pentchevea354462023-07-18 11:15:56 +0300463+ ('', False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300464+
465+ # Export to all
Peter Pentchevea354462023-07-18 11:15:56 +0300466+ ('*', True, constants.ISCSI, _ISCSI_IQN_OURS, True),
467+ ('*', True, constants.ISCSI, _ISCSI_IQN_OURS, True),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300468+
469+ # Only export to the controller
Peter Pentchevea354462023-07-18 11:15:56 +0300470+ ('', False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300471+
472+ # Some of the not-fully-supported pattern lists
Peter Pentchevea354462023-07-18 11:15:56 +0300473+ (_ISCSI_PAT_OTHER, False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
474+ (_ISCSI_PAT_OTHER, False, constants.STORPOOL, _ISCSI_IQN_OTHER, True),
475+ (_ISCSI_PAT_BOTH, False, constants.STORPOOL, _ISCSI_IQN_OURS, True),
476+ (_ISCSI_PAT_BOTH, False, constants.STORPOOL, _ISCSI_IQN_OTHER, True),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300477+ )
478+ @ddt.unpack
Biser Milanovd684c1c2024-11-22 12:12:59 +0200479+ def test_wants_iscsi(self, storpool_iscsi_export_to, use_iscsi,
480+ storage_protocol, hostname, expected):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300481+ """Check the "should this export use iSCSI?" detection."""
Biser Milanovd684c1c2024-11-22 12:12:59 +0200482+ self.cfg.storpool_iscsi_export_to = storpool_iscsi_export_to
Peter Pentchev9c24be92022-09-26 22:35:24 +0300483+ self._setup_test_driver()
484+ self.assertEqual(self.driver._use_iscsi, use_iscsi)
485+
486+ # Make sure the driver reports the correct protocol in the stats
487+ self.driver._update_volume_stats()
488+ self.assertEqual(self.driver._stats["vendor_name"], "StorPool")
489+ self.assertEqual(self.driver._stats["storage_protocol"],
490+ storage_protocol)
491+
492+ def check(conn, forced, expected):
493+ """Pass partially or completely valid connector info."""
494+ for initiator in (None, hostname):
Peter Pentchevea354462023-07-18 11:15:56 +0300495+ for host in (None, _ISCSI_IQN_THIRD):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300496+ self.assertEqual(
497+ self.driver._connector_wants_iscsi({
498+ "host": host,
499+ "initiator": initiator,
500+ **conn,
501+ }),
502+ expected if initiator is not None and host is not None
503+ else forced)
504+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200505+ # If storpool_iscsi_cinder_volume is set and this is the controller,
506+ # then yes.
Peter Pentchev9c24be92022-09-26 22:35:24 +0300507+ check({"storpool_wants_iscsi": True}, True, True)
508+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200509+ # If storpool_iscsi_cinder_volume is not set or this is not the
510+ # controller, then look at the specified expected value.
Peter Pentchev9c24be92022-09-26 22:35:24 +0300511+ check({"storpool_wants_iscsi": False}, use_iscsi, expected)
512+ check({}, use_iscsi, expected)
Peter Pentchevea354462023-07-18 11:15:56 +0300513+
514+ def _validate_iscsi_config(
515+ self,
Biser Milanovda1b0682024-11-29 09:37:10 +0200516+ cfg: MockIscsiConfig,
Peter Pentchevea354462023-07-18 11:15:56 +0300517+ res: dict[str, Any],
518+ tcase: IscsiTestCase,
519+ ) -> None:
520+ """Make sure the returned structure makes sense."""
521+ initiator = res['initiator']
Biser Milanovda1b0682024-11-29 09:37:10 +0200522+ cfg_initiator = cfg.initiators.get('1')
Peter Pentchevea354462023-07-18 11:15:56 +0300523+
Biser Milanovda1b0682024-11-29 09:37:10 +0200524+ self.assertIs(res['cfg'].iscsi, cfg)
525+ self.assertEqual(res['pg'].name, _ISCSI_PORTAL_GROUP)
Peter Pentchevea354462023-07-18 11:15:56 +0300526+
527+ if tcase.initiator is None:
528+ self.assertIsNone(initiator)
529+ else:
530+ self.assertIsNotNone(initiator)
531+ self.assertEqual(initiator, cfg_initiator)
532+
533+ if tcase.volume is None:
534+ self.assertIsNone(res['target'])
535+ else:
536+ self.assertIsNotNone(res['target'])
Biser Milanovda1b0682024-11-29 09:37:10 +0200537+ self.assertEqual(res['target'], cfg.targets.get('1'))
Peter Pentchevea354462023-07-18 11:15:56 +0300538+
539+ if tcase.initiator is None:
540+ self.assertIsNone(cfg_initiator)
541+ self.assertIsNone(res['export'])
542+ else:
543+ self.assertIsNotNone(cfg_initiator)
544+ if tcase.exported:
545+ self.assertIsNotNone(res['export'])
Biser Milanovda1b0682024-11-29 09:37:10 +0200546+ self.assertEqual(res['export'], cfg_initiator.exports[0])
Peter Pentchevea354462023-07-18 11:15:56 +0300547+ else:
548+ self.assertIsNone(res['export'])
549+
550+ @ddt.data(*_ISCSI_TEST_CASES)
551+ def test_iscsi_get_config(self, tcase: IscsiTestCase) -> None:
552+ """Make sure the StorPool iSCSI configuration is parsed correctly."""
553+ cfg_orig = MockIscsiConfig.build(tcase)
554+ configs = [cfg_orig]
555+ iapi = MockIscsiAPI(configs, self)
Biser Milanovda1b0682024-11-29 09:37:10 +0200556+ with mock.patch.object(self.driver._attach, 'api', new=lambda: iapi):
Peter Pentchevea354462023-07-18 11:15:56 +0300557+ res = self.driver._get_iscsi_config(
558+ _ISCSI_IQN_OURS,
Biser Milanovec3bf982024-11-27 17:42:17 +0200559+ fake_constants.VOLUME_ID,
Peter Pentchevea354462023-07-18 11:15:56 +0300560+ )
561+
562+ self._validate_iscsi_config(cfg_orig, res, tcase)
563+
564+ @ddt.data(*_ISCSI_TEST_CASES)
565+ def test_iscsi_create_export(self, tcase: IscsiTestCase) -> None:
566+ """Make sure _create_iscsi_export() makes the right API calls."""
567+ cfg_orig = MockIscsiConfig.build(tcase)
568+ configs = [cfg_orig]
569+ iapi = MockIscsiAPI(configs, self)
Biser Milanovda1b0682024-11-29 09:37:10 +0200570+ with mock.patch.object(self.driver._attach, 'api', new=lambda: iapi):
Peter Pentchevea354462023-07-18 11:15:56 +0300571+ self.driver._create_iscsi_export(
572+ {
Biser Milanovec3bf982024-11-27 17:42:17 +0200573+ 'id': fake_constants.VOLUME_ID,
574+ 'display_name': fake_constants.VOLUME_NAME,
Peter Pentchevea354462023-07-18 11:15:56 +0300575+ },
576+ {
577+ # Yeah, okay, so we cheat a little bit here...
578+ 'host': _ISCSI_IQN_OURS + '.hostname',
579+ 'initiator': _ISCSI_IQN_OURS,
580+ },
581+ )
582+
583+ self.assertEqual(len(configs), tcase.commands_count + 1)
584+ cfg_final = configs[-1]
Biser Milanovda1b0682024-11-29 09:37:10 +0200585+ self.assertEqual(cfg_final.initiators['1'].name, _ISCSI_IQN_OURS)
Peter Pentchevea354462023-07-18 11:15:56 +0300586+ self.assertEqual(
Biser Milanovda1b0682024-11-29 09:37:10 +0200587+ cfg_final.initiators['1'].exports[0].target,
Biser Milanovec3bf982024-11-27 17:42:17 +0200588+ targetName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300589+ )
590+ self.assertEqual(
Biser Milanovda1b0682024-11-29 09:37:10 +0200591+ cfg_final.targets['1'].volume,
Biser Milanovec3bf982024-11-27 17:42:17 +0200592+ volumeName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300593+ )
Biser Milanovd684c1c2024-11-22 12:12:59 +0200594+
595+ @ddt.data(*_ISCSI_TEST_CASES)
596+ def test_remove_iscsi_export(self, tcase: IscsiTestCase):
597+ cfg_orig = MockIscsiConfig.build(tcase)
598+ configs = [cfg_orig]
599+ iapi = MockIscsiAPI(configs, self)
600+
Biser Milanovda1b0682024-11-29 09:37:10 +0200601+ def _target_exists(cfg: MockIscsiConfig, volume: str) -> bool:
602+ for name, target in cfg.targets.items():
603+ if target.volume == volumeName(volume):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200604+ return True
605+ return False
606+
Biser Milanovda1b0682024-11-29 09:37:10 +0200607+ def _export_exists(cfg: MockIscsiConfig, volume: str) -> bool:
608+ for name, initiator in cfg.initiators.items():
609+ for export in initiator.exports:
610+ if export.target == targetName(volume):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200611+ return True
612+ return False
613+
614+ if tcase.exported:
615+ self.assertTrue(
Biser Milanovda1b0682024-11-29 09:37:10 +0200616+ _target_exists(iapi.iSCSIConfig().iscsi, tcase.volume))
Biser Milanovd684c1c2024-11-22 12:12:59 +0200617+ self.assertTrue(
Biser Milanovda1b0682024-11-29 09:37:10 +0200618+ _export_exists(iapi.iSCSIConfig().iscsi, tcase.volume))
Biser Milanovd684c1c2024-11-22 12:12:59 +0200619+
Biser Milanovda1b0682024-11-29 09:37:10 +0200620+ with mock.patch.object(self.driver._attach, 'api', new=lambda: iapi):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200621+ self.driver._remove_iscsi_export(
622+ {
Biser Milanovec3bf982024-11-27 17:42:17 +0200623+ 'id': fake_constants.VOLUME_ID,
624+ 'display_name': fake_constants.VOLUME_NAME,
Biser Milanovd684c1c2024-11-22 12:12:59 +0200625+ },
626+ {
627+ 'host': _ISCSI_IQN_OURS + '.hostname',
628+ 'initiator': _ISCSI_IQN_OURS,
629+ },
630+ )
631+
632+ self.assertFalse(
Biser Milanovda1b0682024-11-29 09:37:10 +0200633+ _target_exists(iapi.iSCSIConfig().iscsi, tcase.volume))
Biser Milanovd684c1c2024-11-22 12:12:59 +0200634+ self.assertFalse(
Biser Milanovda1b0682024-11-29 09:37:10 +0200635+ _export_exists(iapi.iSCSIConfig().iscsi, tcase.volume))
Peter Pentchevacaaa382023-02-28 11:26:13 +0200636diff --git a/cinder/volume/drivers/storpool.py b/cinder/volume/drivers/storpool.py
Biser Milanovda1b0682024-11-29 09:37:10 +0200637index a8200a7f1..d931200f6 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +0300638--- a/cinder/volume/drivers/storpool.py
639+++ b/cinder/volume/drivers/storpool.py
Biser Milanovda1b0682024-11-29 09:37:10 +0200640@@ -15,6 +15,7 @@
Peter Pentchev9c24be92022-09-26 22:35:24 +0300641
Biser Milanovda1b0682024-11-29 09:37:10 +0200642 """StorPool block device driver"""
643
Peter Pentchev9c24be92022-09-26 22:35:24 +0300644+import fnmatch
Biser Milanovda1b0682024-11-29 09:37:10 +0200645 import platform
Biser Milanov90b5a142024-11-28 11:33:39 +0200646
Biser Milanovda1b0682024-11-29 09:37:10 +0200647 from oslo_config import cfg
648@@ -43,6 +44,32 @@ if storpool:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300649
650
651 storpool_opts = [
Biser Milanovd684c1c2024-11-22 12:12:59 +0200652+ cfg.BoolOpt('storpool_iscsi_cinder_volume',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300653+ default=False,
654+ help='Let the cinder-volume service use iSCSI instead of '
655+ 'the StorPool block device driver for accessing '
656+ 'StorPool volumes, e.g. when creating a volume from '
657+ 'an image or vice versa.'),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200658+ cfg.StrOpt('storpool_iscsi_export_to',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300659+ default='',
660+ help='Whether to export volumes using iSCSI. '
661+ 'An empty string (the default) makes the driver export '
662+ 'all volumes using the StorPool native network protocol. '
663+ 'The value "*" makes the driver export all volumes using '
Biser Milanovd684c1c2024-11-22 12:12:59 +0200664+ 'iSCSI (see the Cinder StorPool driver documentation for '
665+ 'how this option and ``storpool_iscsi_cinder_volume`` '
666+ 'interact). Any other value leads to an experimental '
667+ 'not fully supported configuration and is interpreted as '
Peter Pentchev9c24be92022-09-26 22:35:24 +0300668+ 'a whitespace-separated list of patterns for IQNs for '
669+ 'hosts that need volumes to be exported via iSCSI, e.g. '
670+ '"iqn.1991-05.com.microsoft:\\*" for Windows hosts.'),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200671+ cfg.BoolOpt('storpool_iscsi_learn_initiator_iqns',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300672+ default=True,
673+ help='Create a StorPool record for a new initiator as soon as '
674+ 'Cinder asks for a volume to be exported to it.'),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200675+ cfg.StrOpt('storpool_iscsi_portal_group',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300676+ default=None,
677+ help='The portal group to export volumes via iSCSI in.'),
678 cfg.StrOpt('storpool_template',
679 default=None,
680 help='The StorPool template for volumes with no type.'),
Biser Milanovda1b0682024-11-29 09:37:10 +0200681@@ -93,9 +120,10 @@ class StorPoolDriver(driver.VolumeDriver):
682 add ignore_errors to the internal _detach_volume() method
683 1.2.3 - Advertise some more driver capabilities.
684 2.0.0 - Implement revert_to_snapshot().
685+ 2.1.0 - Add iSCSI export support.
Biser Milanovd684c1c2024-11-22 12:12:59 +0200686 """
687
Biser Milanovda1b0682024-11-29 09:37:10 +0200688- VERSION = '2.0.0'
689+ VERSION = '2.1.0'
Biser Milanovd684c1c2024-11-22 12:12:59 +0200690 CI_WIKI_NAME = 'StorPool_distributed_storage_CI'
691
692 def __init__(self, *args, **kwargs):
Biser Milanovda1b0682024-11-29 09:37:10 +0200693@@ -105,6 +133,7 @@ class StorPoolDriver(driver.VolumeDriver):
694 self._ourId = None
Peter Pentchev9c24be92022-09-26 22:35:24 +0300695 self._ourIdInt = None
Biser Milanovda1b0682024-11-29 09:37:10 +0200696 self._attach = None
Biser Milanovd684c1c2024-11-22 12:12:59 +0200697+ self._use_iscsi = False
Peter Pentchev9c24be92022-09-26 22:35:24 +0300698
699 @staticmethod
700 def get_driver_options():
Biser Milanovda1b0682024-11-29 09:37:10 +0200701@@ -159,10 +188,327 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300702 raise StorPoolConfigurationInvalid(
703 section=hostname, param='SP_OURID', error=e)
704
705+ def _connector_wants_iscsi(self, connector):
706+ """Should we do this export via iSCSI?
707+
708+ Check the configuration to determine whether this connector is
709+ expected to provide iSCSI exports as opposed to native StorPool
710+ protocol ones. Match the initiator's IQN against the list of
Biser Milanovd684c1c2024-11-22 12:12:59 +0200711+ patterns supplied in the "storpool_iscsi_export_to" configuration
712+ setting.
Peter Pentchev9c24be92022-09-26 22:35:24 +0300713+ """
714+ if connector is None:
715+ return False
716+ if self._use_iscsi:
717+ LOG.debug(' - forcing iSCSI for all exported volumes')
718+ return True
719+ if connector.get('storpool_wants_iscsi'):
720+ LOG.debug(' - forcing iSCSI for the controller')
721+ return True
722+
723+ try:
724+ iqn = connector.get('initiator')
725+ except Exception:
726+ iqn = None
727+ try:
728+ host = connector.get('host')
729+ except Exception:
730+ host = None
731+ if iqn is None or host is None:
732+ LOG.debug(' - this connector certainly does not want iSCSI')
733+ return False
734+
735+ LOG.debug(' - check whether %(host)s (%(iqn)s) wants iSCSI',
736+ {
737+ 'host': host,
738+ 'iqn': iqn,
739+ })
740+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200741+ export_to = self.configuration.storpool_iscsi_export_to
Peter Pentchev9c24be92022-09-26 22:35:24 +0300742+ if export_to is None:
743+ return False
744+
745+ for pat in export_to.split():
746+ LOG.debug(' - matching against %(pat)s', {'pat': pat})
747+ if fnmatch.fnmatch(iqn, pat):
748+ LOG.debug(' - got it!')
749+ return True
750+ LOG.debug(' - nope')
751+ return False
752+
753 def validate_connector(self, connector):
754+ if self._connector_wants_iscsi(connector):
755+ return True
756 return self._storpool_client_id(connector) >= 0
757
758+ def _get_iscsi_config(self, iqn, volume_id):
759+ """Get the StorPool iSCSI config items pertaining to this volume.
760+
761+ Find the elements of the StorPool iSCSI configuration tree that
762+ will be needed to create, ensure, or remove the iSCSI export of
763+ the specified volume to the specified initiator.
764+ """
Biser Milanovda1b0682024-11-29 09:37:10 +0200765+ cfg = self._attach.api().iSCSIConfig()
Peter Pentchev9c24be92022-09-26 22:35:24 +0300766+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200767+ pg_name = self.configuration.storpool_iscsi_portal_group
Peter Pentchev9c24be92022-09-26 22:35:24 +0300768+ pg_found = [
Biser Milanovda1b0682024-11-29 09:37:10 +0200769+ pg for pg in cfg.iscsi.portalGroups.values() if pg.name == pg_name
Peter Pentchev9c24be92022-09-26 22:35:24 +0300770+ ]
771+ if not pg_found:
772+ raise Exception('StorPool Cinder iSCSI configuration error: '
773+ 'no portal group "{pg}"'.format(pg=pg_name))
774+ pg = pg_found[0]
775+
776+ # Do we know about this initiator?
777+ i_found = [
Biser Milanovda1b0682024-11-29 09:37:10 +0200778+ init for init in cfg.iscsi.initiators.values() if init.name == iqn
Peter Pentchev9c24be92022-09-26 22:35:24 +0300779+ ]
780+ if i_found:
781+ initiator = i_found[0]
782+ else:
783+ initiator = None
784+
785+ # Is this volume already being exported?
Biser Milanovda1b0682024-11-29 09:37:10 +0200786+ volname = self._attach.volumeName(volume_id)
Peter Pentchev9c24be92022-09-26 22:35:24 +0300787+ t_found = [
Biser Milanovda1b0682024-11-29 09:37:10 +0200788+ tgt for tgt in cfg.iscsi.targets.values() if tgt.volume == volname
Peter Pentchev9c24be92022-09-26 22:35:24 +0300789+ ]
790+ if t_found:
791+ target = t_found[0]
792+ else:
793+ target = None
794+
795+ # OK, so is this volume being exported to this initiator?
796+ export = None
797+ if initiator is not None and target is not None:
798+ e_found = [
Biser Milanovda1b0682024-11-29 09:37:10 +0200799+ exp for exp in initiator.exports
800+ if exp.portalGroup == pg.name and exp.target == target.name
Peter Pentchev9c24be92022-09-26 22:35:24 +0300801+ ]
802+ if e_found:
803+ export = e_found[0]
804+
805+ return {
806+ 'cfg': cfg,
807+ 'pg': pg,
808+ 'initiator': initiator,
809+ 'target': target,
810+ 'export': export,
811+ 'volume_name': volname,
812+ 'volume_id': volume_id,
813+ }
814+
815+ def _create_iscsi_export(self, volume, connector):
816+ """Create (if needed) an iSCSI export for the StorPool volume."""
817+ LOG.debug(
818+ '_create_iscsi_export() invoked for volume '
819+ '"%(vol_name)s" (%(vol_id)s) connector %(connector)s',
820+ {
821+ 'vol_name': volume['display_name'],
822+ 'vol_id': volume['id'],
823+ 'connector': connector,
824+ }
825+ )
826+ iqn = connector['initiator']
827+ try:
828+ cfg = self._get_iscsi_config(iqn, volume['id'])
829+ except Exception as exc:
830+ LOG.error(
831+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
832+ )
833+ raise
834+
835+ if cfg['initiator'] is None:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200836+ if not (self.configuration.storpool_iscsi_learn_initiator_iqns or
837+ self.configuration.storpool_iscsi_cinder_volume and
Peter Pentchev9c24be92022-09-26 22:35:24 +0300838+ connector.get('storpool_wants_iscsi')):
839+ raise Exception('The "{iqn}" initiator IQN for the "{host}" '
840+ 'host is not defined in the StorPool '
841+ 'configuration.'
842+ .format(iqn=iqn, host=connector['host']))
843+ else:
844+ LOG.info('Creating a StorPool iSCSI initiator '
845+ 'for "{host}s" ({iqn}s)',
846+ {'host': connector['host'], 'iqn': iqn})
Biser Milanovda1b0682024-11-29 09:37:10 +0200847+ self._attach.api().iSCSIConfigChange({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300848+ 'commands': [
849+ {
850+ 'createInitiator': {
851+ 'name': iqn,
852+ 'username': '',
853+ 'secret': '',
854+ },
855+ },
856+ {
857+ 'initiatorAddNetwork': {
858+ 'initiator': iqn,
859+ 'net': '0.0.0.0/0',
860+ },
861+ },
862+ ]
863+ })
864+
865+ if cfg['target'] is None:
866+ LOG.info(
867+ 'Creating a StorPool iSCSI target '
868+ 'for the "%(vol_name)s" volume (%(vol_id)s)',
869+ {
870+ 'vol_name': volume['display_name'],
871+ 'vol_id': volume['id'],
872+ }
873+ )
Biser Milanovda1b0682024-11-29 09:37:10 +0200874+ self._attach.api().iSCSIConfigChange({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300875+ 'commands': [
876+ {
877+ 'createTarget': {
878+ 'volumeName': cfg['volume_name'],
879+ },
880+ },
881+ ]
882+ })
883+ cfg = self._get_iscsi_config(iqn, volume['id'])
884+
885+ if cfg['export'] is None:
886+ LOG.info('Creating a StorPool iSCSI export '
887+ 'for the "{vol_name}s" volume ({vol_id}s) '
888+ 'to the "{host}s" initiator ({iqn}s) '
889+ 'in the "{pg}s" portal group',
890+ {
891+ 'vol_name': volume['display_name'],
892+ 'vol_id': volume['id'],
893+ 'host': connector['host'],
894+ 'iqn': iqn,
Biser Milanovda1b0682024-11-29 09:37:10 +0200895+ 'pg': cfg['pg'].name
Peter Pentchev9c24be92022-09-26 22:35:24 +0300896+ })
Biser Milanovda1b0682024-11-29 09:37:10 +0200897+ self._attach.api().iSCSIConfigChange({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300898+ 'commands': [
899+ {
900+ 'export': {
901+ 'initiator': iqn,
Biser Milanovda1b0682024-11-29 09:37:10 +0200902+ 'portalGroup': cfg['pg'].name,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300903+ 'volumeName': cfg['volume_name'],
904+ },
905+ },
906+ ]
907+ })
908+
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200909+ target_portals = [
Biser Milanovda1b0682024-11-29 09:37:10 +0200910+ "{addr}:3260".format(addr=net.address)
911+ for net in cfg['pg'].networks
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200912+ ]
Biser Milanovda1b0682024-11-29 09:37:10 +0200913+ target_iqns = [cfg['target'].name] * len(target_portals)
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200914+ target_luns = [0] * len(target_portals)
915+ if connector.get('multipath', False):
916+ multipath_settings = {
917+ 'target_iqns': target_iqns,
918+ 'target_portals': target_portals,
919+ 'target_luns': target_luns,
920+ }
921+ else:
922+ multipath_settings = {}
923+
Peter Pentchev9c24be92022-09-26 22:35:24 +0300924+ res = {
925+ 'driver_volume_type': 'iscsi',
926+ 'data': {
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200927+ **multipath_settings,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300928+ 'target_discovered': False,
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200929+ 'target_iqn': target_iqns[0],
930+ 'target_portal': target_portals[0],
931+ 'target_lun': target_luns[0],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300932+ 'volume_id': volume['id'],
933+ 'discard': True,
934+ },
935+ }
936+ LOG.debug('returning %(res)s', {'res': res})
937+ return res
938+
939+ def _remove_iscsi_export(self, volume, connector):
940+ """Remove an iSCSI export for the specified StorPool volume."""
941+ LOG.debug(
942+ '_remove_iscsi_export() invoked for volume '
943+ '"%(vol_name)s" (%(vol_id)s) connector %(conn)s',
944+ {
945+ 'vol_name': volume['display_name'],
946+ 'vol_id': volume['id'],
947+ 'conn': connector,
948+ }
949+ )
950+ try:
951+ cfg = self._get_iscsi_config(connector['initiator'], volume['id'])
952+ except Exception as exc:
953+ LOG.error(
954+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
955+ )
956+ raise
957+
958+ if cfg['export'] is not None:
959+ LOG.info('Removing the StorPool iSCSI export '
960+ 'for the "%(vol_name)s" volume (%(vol_id)s) '
961+ 'to the "%(host)s" initiator (%(iqn)s) '
962+ 'in the "%(pg)s" portal group',
963+ {
964+ 'vol_name': volume['display_name'],
965+ 'vol_id': volume['id'],
966+ 'host': connector['host'],
967+ 'iqn': connector['initiator'],
Biser Milanovda1b0682024-11-29 09:37:10 +0200968+ 'pg': cfg['pg'].name,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300969+ })
970+ try:
Biser Milanovda1b0682024-11-29 09:37:10 +0200971+ self._attach.api().iSCSIConfigChange({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300972+ 'commands': [
973+ {
974+ 'exportDelete': {
Biser Milanovda1b0682024-11-29 09:37:10 +0200975+ 'initiator': cfg['initiator'].name,
976+ 'portalGroup': cfg['pg'].name,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300977+ 'volumeName': cfg['volume_name'],
978+ },
979+ },
980+ ]
981+ })
Biser Milanovda1b0682024-11-29 09:37:10 +0200982+ except spapi.ApiError as e:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300983+ if e.name not in ('objectExists', 'objectDoesNotExist'):
984+ raise
985+ LOG.info('Looks like somebody beat us to it')
986+
987+ if cfg['target'] is not None:
988+ last = True
Biser Milanovda1b0682024-11-29 09:37:10 +0200989+ for initiator in cfg['cfg'].iscsi.initiators.values():
990+ if initiator.name == cfg['initiator'].name:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300991+ continue
Biser Milanovda1b0682024-11-29 09:37:10 +0200992+ for exp in initiator.exports:
993+ if exp.target == cfg['target'].name:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300994+ last = False
995+ break
996+ if not last:
997+ break
998+
999+ if last:
1000+ LOG.info(
1001+ 'Removing the StorPool iSCSI target '
1002+ 'for the "{vol_name}s" volume ({vol_id}s)',
1003+ {
1004+ 'vol_name': volume['display_name'],
1005+ 'vol_id': volume['id'],
1006+ }
1007+ )
1008+ try:
Biser Milanovda1b0682024-11-29 09:37:10 +02001009+ self._attach.api().iSCSIConfigChange({
Peter Pentchev9c24be92022-09-26 22:35:24 +03001010+ 'commands': [
1011+ {
1012+ 'deleteTarget': {
1013+ 'volumeName': cfg['volume_name'],
1014+ },
1015+ },
1016+ ]
1017+ })
Biser Milanovda1b0682024-11-29 09:37:10 +02001018+ except spapi.ApiError as e:
Peter Pentchev9c24be92022-09-26 22:35:24 +03001019+ if e.name not in ('objectDoesNotExist', 'invalidParam'):
1020+ raise
1021+ LOG.info('Looks like somebody beat us to it')
1022+
1023 def initialize_connection(self, volume, connector):
1024+ if self._connector_wants_iscsi(connector):
1025+ return self._create_iscsi_export(volume, connector)
1026 return {'driver_volume_type': 'storpool',
1027 'data': {
1028 'client_id': self._storpool_client_id(connector),
Biser Milanovda1b0682024-11-29 09:37:10 +02001029@@ -171,6 +517,9 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001030 }}
1031
1032 def terminate_connection(self, volume, connector, **kwargs):
1033+ if self._connector_wants_iscsi(connector):
1034+ LOG.debug('- removing an iSCSI export')
1035+ self._remove_iscsi_export(volume, connector)
1036 pass
1037
1038 def create_snapshot(self, snapshot):
Biser Milanovda1b0682024-11-29 09:37:10 +02001039@@ -272,11 +621,20 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001040 )
1041
1042 def create_export(self, context, volume, connector):
1043- pass
1044+ if self._connector_wants_iscsi(connector):
1045+ LOG.debug('- creating an iSCSI export')
1046+ self._create_iscsi_export(volume, connector)
1047
1048 def remove_export(self, context, volume):
1049 pass
1050
1051+ def _attach_volume(self, context, volume, properties, remote=False):
Biser Milanovd684c1c2024-11-22 12:12:59 +02001052+ if self.configuration.storpool_iscsi_cinder_volume and not remote:
Peter Pentchev9c24be92022-09-26 22:35:24 +03001053+ LOG.debug('- adding the "storpool_wants_iscsi" flag')
1054+ properties['storpool_wants_iscsi'] = True
1055+
1056+ return super()._attach_volume(context, volume, properties, remote)
1057+
1058 def delete_volume(self, volume):
Biser Milanovda1b0682024-11-29 09:37:10 +02001059 name = self._attach.volumeName(volume['id'])
Peter Pentchev9c24be92022-09-26 22:35:24 +03001060 try:
Biser Milanovda1b0682024-11-29 09:37:10 +02001061@@ -313,6 +671,17 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001062 LOG.error("StorPoolDriver API initialization failed: %s", e)
1063 raise
1064
Biser Milanovd684c1c2024-11-22 12:12:59 +02001065+ export_to = self.configuration.storpool_iscsi_export_to
Peter Pentchev9c24be92022-09-26 22:35:24 +03001066+ export_to_set = export_to is not None and export_to.split()
Biser Milanovd684c1c2024-11-22 12:12:59 +02001067+ vol_iscsi = self.configuration.storpool_iscsi_cinder_volume
1068+ pg_name = self.configuration.storpool_iscsi_portal_group
Peter Pentchev9c24be92022-09-26 22:35:24 +03001069+ if (export_to_set or vol_iscsi) and pg_name is None:
Biser Milanovd684c1c2024-11-22 12:12:59 +02001070+ msg = _('The "storpool_iscsi_portal_group" option is required if '
1071+ 'any patterns are listed in "storpool_iscsi_export_to"')
Peter Pentchev9c24be92022-09-26 22:35:24 +03001072+ raise exception.VolumeDriverException(message=msg)
1073+
1074+ self._use_iscsi = export_to == "*"
1075+
1076 def _update_volume_stats(self):
1077 try:
Biser Milanovda1b0682024-11-29 09:37:10 +02001078 dl = self._attach.api().disksList()
1079@@ -338,7 +707,7 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001080 'total_capacity_gb': total / units.Gi,
1081 'free_capacity_gb': free / units.Gi,
1082 'reserved_percentage': 0,
1083- 'multiattach': True,
Peter Pentchev5a9f8a62023-12-06 10:40:18 +02001084+ 'multiattach': self._use_iscsi,
Peter Pentchev9c24be92022-09-26 22:35:24 +03001085 'QoS_support': False,
1086 'thick_provisioning_support': False,
1087 'thin_provisioning_support': True,
Biser Milanovda1b0682024-11-29 09:37:10 +02001088@@ -357,7 +726,9 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001089 'volume_backend_name') or 'storpool',
1090 'vendor_name': 'StorPool',
1091 'driver_version': self.VERSION,
1092- 'storage_protocol': constants.STORPOOL,
1093+ 'storage_protocol': (
1094+ constants.ISCSI if self._use_iscsi else constants.STORPOOL
1095+ ),
Peter Pentchevacaaa382023-02-28 11:26:13 +02001096 # Driver capabilities
Peter Pentchev9c24be92022-09-26 22:35:24 +03001097 'clone_across_pools': True,
1098 'sparse_copy_volume': True,
Peter Pentchevacaaa382023-02-28 11:26:13 +02001099diff --git a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
Biser Milanovda1b0682024-11-29 09:37:10 +02001100index d2c5895a9..936e83675 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +03001101--- a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
1102+++ b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
Peter Pentchevacaaa382023-02-28 11:26:13 +02001103@@ -19,12 +19,15 @@ Prerequisites
Peter Pentchev9c24be92022-09-26 22:35:24 +03001104 * The controller and all the compute nodes must have access to the StorPool
1105 API service.
1106
1107-* All nodes where StorPool-backed volumes will be attached must have access to
1108+* If iSCSI is not being used as a transport (see below), all nodes where
1109+ StorPool-backed volumes will be attached must have access to
1110 the StorPool data network and run the ``storpool_block`` service.
1111
1112-* If StorPool-backed Cinder volumes need to be created directly from Glance
1113- images, then the node running the ``cinder-volume`` service must also have
1114- access to the StorPool data network and run the ``storpool_block`` service.
1115+* If Glance uses Cinder as its image store, or if StorPool-backed Cinder
1116+ volumes need to be created directly from Glance images, and iSCSI is not
1117+ being used as a transport, then the node running the ``cinder-volume``
1118+ service must also have access to the StorPool data network and run
1119+ the ``storpool_block`` service.
1120
1121 * All nodes that need to access the StorPool API (the compute nodes and
1122 the node running the ``cinder-volume`` service) must have the following
Biser Milanovda1b0682024-11-29 09:37:10 +02001123@@ -34,6 +37,34 @@ Prerequisites
Peter Pentchev9c24be92022-09-26 22:35:24 +03001124 * the storpool Python bindings package
1125 * the storpool.spopenstack Python helper package
1126
1127+Using iSCSI as the transport protocol
1128+-------------------------------------
1129+
1130+The StorPool distributed storage system uses its own, highly optimized and
1131+tailored for its specifics, network protocol for communication between
1132+the storage servers and the clients (the OpenStack cluster nodes where
1133+StorPool-backed volumes will be attached). There are cases when granting
1134+various nodes access to the StorPool data network or installing and
1135+running the ``storpool_block`` client service on them may pose difficulties.
1136+The StorPool servers may also expose the user-created volumes and snapshots
1137+using the standard iSCSI protocol that only requires TCP routing and
1138+connectivity between the storage servers and the StorPool clients.
1139+The StorPool Cinder driver may be configured to export volumes and
Biser Milanovda1b0682024-11-29 09:37:10 +02001140+snapshots via iSCSI using the ``storpool_iscsi_export_to`` and
1141+``storpool_iscsi_portal_group`` configuration options.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001142+
1143+Additionally, even if e.g. the hypervisor nodes running Nova will use
1144+the StorPool network protocol and run the ``storpool_block`` service
Biser Milanovda1b0682024-11-29 09:37:10 +02001145+(so the ``storpool_iscsi_export_to`` option has its default empty string
1146+value), the ``storpool_iscsi_cinder_volume`` option configures the
1147+StorPool Cinder driver so that only the ``cinder-volume`` service will
1148+use the iSCSI protocol when attaching volumes and snapshots to transfer
1149+data to and from Glance images.
Biser Milanovd684c1c2024-11-22 12:12:59 +02001150+
1151+Multiattach support for StorPool is only enabled if iSCSI is used:
Biser Milanovda1b0682024-11-29 09:37:10 +02001152+``storpool_iscsi_export_to`` is set to ``*``, that is, when all StorPool
1153+volumes will be exported via iSCSI to all initiators.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001154+
1155 Configuring the StorPool volume driver
1156 --------------------------------------
1157
Biser Milanovda1b0682024-11-29 09:37:10 +02001158@@ -55,6 +86,35 @@ volume backend definition) and per volume type:
Peter Pentchev9c24be92022-09-26 22:35:24 +03001159 with the default placement constraints for the StorPool cluster.
1160 The default value for the chain replication is 3.
1161
Peter Pentchevea354462023-07-18 11:15:56 +03001162+In addition, if the iSCSI protocol is used to access the StorPool cluster as
1163+described in the previous section, the following options may be defined in
1164+the ``cinder.conf`` volume backend definition:
1165+
Biser Milanovda1b0682024-11-29 09:37:10 +02001166+- ``storpool_iscsi_export_to``: if set to the value ``*``, the StorPool
1167+ Cinder driver will export volumes and snapshots using the iSCSI
1168+ protocol instead of the StorPool network protocol. The
1169+ ``storpool_iscsi_portal_group`` option must also be specified.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001170+
Biser Milanovda1b0682024-11-29 09:37:10 +02001171+- ``storpool_iscsi_portal_group``: if the ``storpool_iscsi_export_to``
1172+ option is set to the value ``*`` or the
1173+ ``storpool_iscsi_cinder_volume`` option is turned on, this option
1174+ specifies the name of the iSCSI portal group that Cinder volumes will
1175+ be exported to.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001176+
Biser Milanovda1b0682024-11-29 09:37:10 +02001177+- ``storpool_iscsi_cinder_volume``: if enabled, even if the
1178+ ``storpool_iscsi_export_to`` option has its default empty value, the
1179+ ``cinder-volume`` service will use iSCSI to attach the volumes and
1180+ snapshots for transferring data to and from Glance images if Glance is
1181+ configured to use the Cinder glance_store.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001182+
Biser Milanovda1b0682024-11-29 09:37:10 +02001183+- ``storpool_iscsi_learn_initiator_iqns``: if enabled, the StorPool
1184+ Cinder driver will automatically use the StorPool API to create
1185+ definitions for new initiators in the StorPool cluster's
1186+ configuration. This is the default behavior of the driver; it may be
1187+ disabled in the rare case if, e.g. because of site policy, OpenStack
1188+ iSCSI initiators (e.g. Nova hypervisors) need to be explicitly allowed
1189+ to use the StorPool iSCSI targets.
Peter Pentchevea354462023-07-18 11:15:56 +03001190+
Peter Pentchev9c24be92022-09-26 22:35:24 +03001191 Using the StorPool volume driver
1192 --------------------------------
1193
Biser Milanovda1b0682024-11-29 09:37:10 +02001194diff --git a/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml b/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
1195new file mode 100644
1196index 000000000..3863e4099
1197--- /dev/null
1198+++ b/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
1199@@ -0,0 +1,15 @@
1200+features:
1201+ - |
1202+ StorPool driver: Added support for exporting the StorPool-backed
1203+ volumes using the iSCSI protocol, so that the Cinder volume service
1204+ and/or the Nova or Glance consumers do not need to have the StorPool
1205+ block device third-party service installed. See the StorPool driver
1206+ section in the Cinder documentation for more information on the
1207+ ``storpool_iscsi_export_to``, ``storpool_iscsi_portal_group``,
1208+ ``storpool_iscsi_cinder_volume``, and
1209+ ``storpool_iscsi_learn_initiator_iqns`` options.
1210+
1211+ .. note::
1212+ Multiattach support for StorPool is now only enabled if
1213+ ``storpool_iscsi_export_to`` is set to ``*``, that is, when all
1214+ StorPool volumes will be exported via iSCSI to all initiators.
Peter Pentchevacaaa382023-02-28 11:26:13 +02001215--
Biser Milanovd684c1c2024-11-22 12:12:59 +020012162.43.0
Peter Pentchevacaaa382023-02-28 11:26:13 +02001217