blob: a7dbea7c4057f9264f51780dd9cbb58019031d29 [file] [log] [blame]
Biser Milanov233be152025-01-24 17:35:11 +02001From f15732ac9c326996b418aaaf40aece68912f038b Mon Sep 17 00:00:00 2001
Peter Pentchev9c24be92022-09-26 22:35:24 +03002From: Peter Penchev <openstack-dev@storpool.com>
Biser Milanov8323a8c2024-11-20 10:53:27 +02003Date: Mon, 12 Mar 2018 12:00:10 +0200
Biser Milanovd684c1c2024-11-22 12:12:59 +02004Subject: [PATCH] Add iSCSI export support to the StorPool driver
Peter Pentchev9c24be92022-09-26 22:35:24 +03005
6Add four new driver options:
Biser Milanovd684c1c2024-11-22 12:12:59 +02007- storpool_iscsi_cinder_volume: use StorPool iSCSI attachments whenever
Peter Pentchev9c24be92022-09-26 22:35:24 +03008 the cinder-volume service needs to attach a volume to the controller,
9 e.g. for copying an image to a volume or vice versa
Biser Milanovd684c1c2024-11-22 12:12:59 +020010- storpool_iscsi_export_to:
11 - an empty string to use the StorPool native protocol for exporting
12 volumes
Peter Pentchev9c24be92022-09-26 22:35:24 +030013 - the string "*" to always use iSCSI for exporting volumes
14 - an experimental, not fully supported list of IQN patterns to export
15 volumes to using iSCSI; this results in a Cinder driver that exports
16 different volumes using different storage protocols
Biser Milanovd684c1c2024-11-22 12:12:59 +020017- storpool_iscsi_portal_group: the name of the iSCSI portal group
18 defined in the StorPool configuration to use for these export
19- storpool_iscsi_learn_initiator_iqns: automatically create StorPool
20 configuration records for an initiator when a volume is first exported
21 to it
Peter Pentchev9c24be92022-09-26 22:35:24 +030022
Biser Milanovd684c1c2024-11-22 12:12:59 +020023When exporting volumes via iSCSI, report the storage protocol as
24"iSCSI".
Peter Pentchev9c24be92022-09-26 22:35:24 +030025
26Change-Id: I9de64306e0e6976268df782053b0651dd1cca96f
27---
Biser Milanov233be152025-01-24 17:35:11 +020028 .../unit/volume/drivers/test_storpool.py | 441 +++++++++++++++++-
29 cinder/volume/drivers/storpool.py | 381 ++++++++++++++-
Biser Milanovda1b0682024-11-29 09:37:10 +020030 .../drivers/storpool-volume-driver.rst | 68 ++-
31 .../storpool-iscsi-cefcfe590a07c5c7.yaml | 15 +
Biser Milanov233be152025-01-24 17:35:11 +020032 4 files changed, 889 insertions(+), 16 deletions(-)
Biser Milanovda1b0682024-11-29 09:37:10 +020033 create mode 100644 releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
Biser Milanovc0ee3582025-01-22 09:09:24 +000034
Peter Pentchevacaaa382023-02-28 11:26:13 +020035diff --git a/cinder/tests/unit/volume/drivers/test_storpool.py b/cinder/tests/unit/volume/drivers/test_storpool.py
Biser Milanov233be152025-01-24 17:35:11 +020036index 2015c734d..440740a56 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 Milanov233be152025-01-24 17:35:11 +020039@@ -13,9 +13,10 @@
40 # License for the specific language governing permissions and limitations
Biser Milanovda1b0682024-11-29 09:37:10 +020041 # under the License.
Peter Pentchevea354462023-07-18 11:15:56 +030042
Biser Milanov233be152025-01-24 17:35:11 +020043-
Peter Pentchevea354462023-07-18 11:15:56 +030044+import dataclasses
45 import itertools
46 import re
Peter Pentchevea354462023-07-18 11:15:56 +030047+from typing import Any, NamedTuple, TYPE_CHECKING # noqa: H301
48 from unittest import mock
49
50 import ddt
Biser Milanov233be152025-01-24 17:35:11 +020051@@ -23,6 +24,7 @@ from os_brick.initiator import storpool_utils
52 from os_brick.tests.initiator import test_storpool_utils
Peter Pentchevea354462023-07-18 11:15:56 +030053 from oslo_utils import units
Peter Pentchevea354462023-07-18 11:15:56 +030054
Peter Pentchev9c24be92022-09-26 22:35:24 +030055+from cinder.common import constants
56 from cinder import exception
Biser Milanovec3bf982024-11-27 17:42:17 +020057 from cinder.tests.unit import fake_constants
Peter Pentchev9c24be92022-09-26 22:35:24 +030058 from cinder.tests.unit import test
Biser Milanov233be152025-01-24 17:35:11 +020059@@ -30,6 +32,13 @@ from cinder.volume import configuration as conf
Biser Milanovda1b0682024-11-29 09:37:10 +020060 from cinder.volume.drivers import storpool as driver
61
Peter Pentchevea354462023-07-18 11:15:56 +030062
Biser Milanov233be152025-01-24 17:35:11 +020063+ISCSI_IQN_OURS = 'beleriand'
64+ISCSI_IQN_OTHER = 'rohan'
65+ISCSI_IQN_THIRD = 'gondor'
66+ISCSI_PAT_OTHER = 'roh*'
67+ISCSI_PAT_BOTH = '*riand roh*'
68+ISCSI_PORTAL_GROUP = 'openstack_pg'
Biser Milanovc0ee3582025-01-22 09:09:24 +000069+
70 volume_types = {
71 fake_constants.VOLUME_TYPE_ID: {},
72 fake_constants.VOLUME_TYPE2_ID: {'storpool_template': 'ssd'},
Biser Milanov233be152025-01-24 17:35:11 +020073@@ -67,6 +76,10 @@ def snapshotName(vtype, vid, more=None):
74 )
Peter Pentchevea354462023-07-18 11:15:56 +030075
76
77+def targetName(vid):
78+ return 'iqn.2012-11.storpool:{id}'.format(id=vid)
79+
80+
Biser Milanov233be152025-01-24 17:35:11 +020081 class MockAPI(object):
82 def __init__(self, *args):
83 self._disks = {}
84@@ -162,6 +175,242 @@ class MockAPI(object):
85 volumes[name] = dict(snapshots[snapname])
Peter Pentchevea354462023-07-18 11:15:56 +030086
Biser Milanovc0ee3582025-01-22 09:09:24 +000087
Peter Pentchevea354462023-07-18 11:15:56 +030088+class IscsiTestCase(NamedTuple):
89+ """A single test case for the iSCSI config and export methods."""
90+
91+ initiator: str | None
92+ volume: str | None
93+ exported: bool
94+ commands_count: int
95+
96+
97+@dataclasses.dataclass(frozen=True)
98+class MockIscsiConfig:
99+ """Mock the structure returned by the "get current config" query."""
100+
Peter Pentchevea354462023-07-18 11:15:56 +0300101+ @classmethod
Biser Milanov233be152025-01-24 17:35:11 +0200102+ def build(cls, tcase: IscsiTestCase) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300103+ """Build a test config structure."""
104+ initiators = {
Biser Milanov233be152025-01-24 17:35:11 +0200105+ '0': {'name': ISCSI_IQN_OTHER, 'exports': []},
Peter Pentchevea354462023-07-18 11:15:56 +0300106+ }
107+ if tcase.initiator is not None:
Biser Milanov233be152025-01-24 17:35:11 +0200108+ initiators['1'] = {
109+ 'name': tcase.initiator,
110+ 'exports': (
Peter Pentchevea354462023-07-18 11:15:56 +0300111+ [
Biser Milanov233be152025-01-24 17:35:11 +0200112+ {
113+ 'portalGroup': ISCSI_PORTAL_GROUP,
114+ 'target': targetName(tcase.volume),
115+ },
Peter Pentchevea354462023-07-18 11:15:56 +0300116+ ]
117+ if tcase.exported
118+ else []
119+ ),
Biser Milanov233be152025-01-24 17:35:11 +0200120+ }
Peter Pentchevea354462023-07-18 11:15:56 +0300121+
122+ targets = {
Biser Milanov233be152025-01-24 17:35:11 +0200123+ '0': {
124+ 'name': targetName(fake_constants.VOLUME2_ID),
125+ 'volume': volumeName(fake_constants.VOLUME2_ID),
126+ },
Peter Pentchevea354462023-07-18 11:15:56 +0300127+ }
128+ if tcase.volume is not None:
Biser Milanov233be152025-01-24 17:35:11 +0200129+ targets['1'] = {
130+ 'name': targetName(tcase.volume),
131+ 'volume': volumeName(tcase.volume),
132+ }
Peter Pentchevea354462023-07-18 11:15:56 +0300133+
Biser Milanov233be152025-01-24 17:35:11 +0200134+ return {
135+
136+ 'portalGroups': {
137+ '0': {
138+ 'name': ISCSI_PORTAL_GROUP + '-not',
139+ 'networks': [],
140+ },
141+ '1': {
142+ 'name': ISCSI_PORTAL_GROUP,
143+ 'networks': [
144+ {'address': "192.0.2.0"},
145+ {'address': "195.51.100.0"},
Peter Pentchevea354462023-07-18 11:15:56 +0300146+ ],
Biser Milanov233be152025-01-24 17:35:11 +0200147+ },
Peter Pentchevea354462023-07-18 11:15:56 +0300148+ },
Biser Milanov233be152025-01-24 17:35:11 +0200149+ 'initiators': initiators,
150+ 'targets': targets,
Peter Pentchevea354462023-07-18 11:15:56 +0300151+
Biser Milanov233be152025-01-24 17:35:11 +0200152+ }
Peter Pentchevea354462023-07-18 11:15:56 +0300153+
Peter Pentchevea354462023-07-18 11:15:56 +0300154+
155+
156+class MockIscsiAPI:
157+ """Mock only the iSCSI-related calls of the StorPool API bindings."""
158+
159+ _asrt: test.TestCase
Biser Milanov233be152025-01-24 17:35:11 +0200160+ _configs: list[dict]
Peter Pentchevea354462023-07-18 11:15:56 +0300161+
162+ def __init__(
163+ self,
Biser Milanov233be152025-01-24 17:35:11 +0200164+ configs: list[dict],
Peter Pentchevea354462023-07-18 11:15:56 +0300165+ asrt: test.TestCase,
166+ ) -> None:
167+ """Store the reference to the list of iSCSI config objects."""
168+ self._asrt = asrt
169+ self._configs = configs
170+
Biser Milanov233be152025-01-24 17:35:11 +0200171+ def get_iscsi_config(self) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300172+ """Return the last version of the iSCSI configuration."""
Biser Milanov233be152025-01-24 17:35:11 +0200173+ return {'iscsi': self._configs[-1]}
Peter Pentchevea354462023-07-18 11:15:56 +0300174+
Biser Milanov233be152025-01-24 17:35:11 +0200175+ def _handle_export(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300176+ """Add an export for an initiator."""
177+ self._asrt.assertDictEqual(
178+ cmd,
179+ {
Biser Milanov233be152025-01-24 17:35:11 +0200180+ 'initiator': ISCSI_IQN_OURS,
181+ 'portalGroup': ISCSI_PORTAL_GROUP,
Biser Milanovec3bf982024-11-27 17:42:17 +0200182+ 'volumeName': volumeName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300183+ },
184+ )
Biser Milanov233be152025-01-24 17:35:11 +0200185+ self._asrt.assertEqual(cfg['initiators']['1']['name'], cmd['initiator'])
186+ self._asrt.assertListEqual(cfg['initiators']['1']['exports'], [])
Peter Pentchevea354462023-07-18 11:15:56 +0300187+
Biser Milanov233be152025-01-24 17:35:11 +0200188+ cfg['initiators'] = {
189+ **cfg['initiators'],
190+ '1': {
191+ **cfg['initiators']['1'],
192+ 'exports': [
193+ {
194+ 'portalGroup': cmd['portalGroup'],
195+ 'target': targetName(fake_constants.VOLUME_ID),
196+ },
197+ ],
Peter Pentchevea354462023-07-18 11:15:56 +0300198+ },
Biser Milanov233be152025-01-24 17:35:11 +0200199+ }
200+ return cfg
Peter Pentchevea354462023-07-18 11:15:56 +0300201+
Biser Milanov233be152025-01-24 17:35:11 +0200202+ def _handle_delete_export(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200203+ """Delete an export for an initiator."""
204+ self._asrt.assertDictEqual(
205+ cmd,
206+ {
Biser Milanov233be152025-01-24 17:35:11 +0200207+ 'initiator': ISCSI_IQN_OURS,
208+ 'portalGroup': ISCSI_PORTAL_GROUP,
Biser Milanovec3bf982024-11-27 17:42:17 +0200209+ 'volumeName': volumeName(fake_constants.VOLUME_ID),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200210+ },
211+ )
Biser Milanov233be152025-01-24 17:35:11 +0200212+ self._asrt.assertEqual(cfg['initiators']['1']['name'], cmd['initiator'])
Biser Milanovd684c1c2024-11-22 12:12:59 +0200213+ self._asrt.assertListEqual(
Biser Milanov233be152025-01-24 17:35:11 +0200214+ cfg['initiators']['1']['exports'],
215+ [{'portalGroup': ISCSI_PORTAL_GROUP,
216+ 'target': cfg['targets']['1']['name']}])
Biser Milanovd684c1c2024-11-22 12:12:59 +0200217+
Biser Milanov233be152025-01-24 17:35:11 +0200218+ del cfg['initiators']['1']
219+ return cfg
Biser Milanovd684c1c2024-11-22 12:12:59 +0200220+
Biser Milanov233be152025-01-24 17:35:11 +0200221+ def _handle_create_initiator(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300222+ """Add a whole new initiator."""
223+ self._asrt.assertDictEqual(
224+ cmd,
225+ {
Biser Milanov233be152025-01-24 17:35:11 +0200226+ 'name': ISCSI_IQN_OURS,
Peter Pentchevea354462023-07-18 11:15:56 +0300227+ 'username': '',
228+ 'secret': '',
229+ },
230+ )
231+ self._asrt.assertNotIn(
232+ cmd['name'],
Biser Milanov233be152025-01-24 17:35:11 +0200233+ [init['name'] for init in cfg['initiators'].values()],
Peter Pentchevea354462023-07-18 11:15:56 +0300234+ )
Biser Milanov233be152025-01-24 17:35:11 +0200235+ self._asrt.assertListEqual(sorted(cfg['initiators']), ['0'])
Peter Pentchevea354462023-07-18 11:15:56 +0300236+
Biser Milanov233be152025-01-24 17:35:11 +0200237+ cfg['initiators'] = {
238+ **cfg['initiators'],
239+ '1': {'name': cmd['name'], 'exports': []},
240+ }
241+ return cfg
Peter Pentchevea354462023-07-18 11:15:56 +0300242+
Biser Milanov233be152025-01-24 17:35:11 +0200243+
244+ def _handle_create_target(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300245+ """Add a target for a volume so that it may be exported."""
246+ self._asrt.assertDictEqual(
247+ cmd,
Biser Milanovec3bf982024-11-27 17:42:17 +0200248+ {'volumeName': volumeName(fake_constants.VOLUME_ID)},
Peter Pentchevea354462023-07-18 11:15:56 +0300249+ )
Biser Milanov233be152025-01-24 17:35:11 +0200250+ self._asrt.assertListEqual(sorted(cfg['targets']), ['0'])
251+ cfg['targets'] = {
252+ **cfg['targets'],
253+ '1': {
254+ 'name': targetName(fake_constants.VOLUME_ID),
255+ 'volume': volumeName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300256+ },
Biser Milanov233be152025-01-24 17:35:11 +0200257+ }
258+ return cfg
Peter Pentchevea354462023-07-18 11:15:56 +0300259+
Biser Milanov233be152025-01-24 17:35:11 +0200260+ def _handle_delete_target(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200261+ """Remove a target for a volume."""
262+ self._asrt.assertDictEqual(
263+ cmd,
Biser Milanovec3bf982024-11-27 17:42:17 +0200264+ {'volumeName': volumeName(fake_constants.VOLUME_ID)},
Biser Milanovd684c1c2024-11-22 12:12:59 +0200265+ )
266+
Biser Milanov233be152025-01-24 17:35:11 +0200267+ self._asrt.assertListEqual(sorted(cfg['targets']), ['0', '1'])
268+ del cfg['targets']['1']
269+ return cfg
Biser Milanovd684c1c2024-11-22 12:12:59 +0200270+
Peter Pentchevea354462023-07-18 11:15:56 +0300271+ def _handle_initiator_add_network(
272+ self,
Biser Milanov233be152025-01-24 17:35:11 +0200273+ cfg: dict,
Peter Pentchevea354462023-07-18 11:15:56 +0300274+ cmd: dict[str, Any],
Biser Milanov233be152025-01-24 17:35:11 +0200275+ ) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300276+ """Add a network that an initiator is allowed to log in from."""
277+ self._asrt.assertDictEqual(
278+ cmd,
279+ {
Biser Milanov233be152025-01-24 17:35:11 +0200280+ 'initiator': ISCSI_IQN_OURS,
Peter Pentchevea354462023-07-18 11:15:56 +0300281+ 'net': '0.0.0.0/0',
282+ },
283+ )
Biser Milanov233be152025-01-24 17:35:11 +0200284+ return cfg
Peter Pentchevea354462023-07-18 11:15:56 +0300285+
286+ _CMD_HANDLERS = {
287+ 'createInitiator': _handle_create_initiator,
288+ 'createTarget': _handle_create_target,
Biser Milanovd684c1c2024-11-22 12:12:59 +0200289+ 'deleteTarget': _handle_delete_target,
Peter Pentchevea354462023-07-18 11:15:56 +0300290+ 'export': _handle_export,
Biser Milanovd684c1c2024-11-22 12:12:59 +0200291+ 'exportDelete': _handle_delete_export,
Peter Pentchevea354462023-07-18 11:15:56 +0300292+ 'initiatorAddNetwork': _handle_initiator_add_network,
293+ }
294+
Biser Milanov233be152025-01-24 17:35:11 +0200295+ def post_iscsi_config(
Peter Pentchevea354462023-07-18 11:15:56 +0300296+ self,
297+ commands: dict[str, list[dict[str, dict[str, Any]]]],
298+ ) -> None:
299+ """Apply the requested changes to the iSCSI configuration.
300+
301+ This method adds a new config object to the configs list,
302+ making a shallow copy of the last one and applying the changes
303+ specified in the list of commands.
304+ """
305+ self._asrt.assertListEqual(sorted(commands), ['commands'])
306+ self._asrt.assertGreater(len(commands['commands']), 0)
307+ for cmd in commands['commands']:
308+ keys = sorted(cmd.keys())
309+ cmd_name = keys[0]
310+ self._asrt.assertListEqual(keys, [cmd_name])
311+ handler = self._CMD_HANDLERS[cmd_name]
312+ new_cfg = handler(self, self._configs[-1], cmd[cmd_name])
313+ self._configs.append(new_cfg)
314+
315+
316+_ISCSI_TEST_CASES = [
317+ IscsiTestCase(None, None, False, 4),
Biser Milanov233be152025-01-24 17:35:11 +0200318+ IscsiTestCase(ISCSI_IQN_OURS, None, False, 2),
319+ IscsiTestCase(ISCSI_IQN_OURS, fake_constants.VOLUME_ID, False, 1),
320+ IscsiTestCase(ISCSI_IQN_OURS, fake_constants.VOLUME_ID, True, 0),
Peter Pentchevea354462023-07-18 11:15:56 +0300321+]
322+
Biser Milanovc0ee3582025-01-22 09:09:24 +0000323+
Biser Milanov233be152025-01-24 17:35:11 +0200324 class MockVolumeDB(object):
325 """Simulate a Cinder database with a volume_get() method."""
326
327@@ -198,7 +447,15 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300328 self.cfg.volume_backend_name = 'storpool_test'
329 self.cfg.storpool_template = None
330 self.cfg.storpool_replication = 3
Biser Milanovd684c1c2024-11-22 12:12:59 +0200331+ self.cfg.storpool_iscsi_cinder_volume = False
332+ self.cfg.storpool_iscsi_export_to = ''
333+ self.cfg.storpool_iscsi_learn_initiator_iqns = True
Biser Milanov233be152025-01-24 17:35:11 +0200334+ self.cfg.storpool_iscsi_portal_group = ISCSI_PORTAL_GROUP
Biser Milanovc0ee3582025-01-22 09:09:24 +0000335+
Biser Milanov233be152025-01-24 17:35:11 +0200336+ self._setup_test_driver()
337
Peter Pentchev9c24be92022-09-26 22:35:24 +0300338+ def _setup_test_driver(self):
339+ """Initialize a StorPool driver as per the current configuration."""
340 mock_exec = mock.Mock()
341 mock_exec.return_value = ('', '')
342
Biser Milanov233be152025-01-24 17:35:11 +0200343@@ -216,7 +473,7 @@ class StorPoolTestCase(test.TestCase):
344 self.driver.check_for_setup_error()
Peter Pentchev9c24be92022-09-26 22:35:24 +0300345
346 @ddt.data(
347- (5, TypeError),
348+ (5, (TypeError, AttributeError)),
349 ({'no-host': None}, KeyError),
350 ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
351 ({'host': 's01'}, None),
Biser Milanov233be152025-01-24 17:35:11 +0200352@@ -232,7 +489,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300353 conn)
354
355 @ddt.data(
356- (5, TypeError),
357+ (5, (TypeError, AttributeError)),
358 ({'no-host': None}, KeyError),
359 ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
360 )
Biser Milanov233be152025-01-24 17:35:11 +0200361@@ -271,7 +528,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev5a9f8a62023-12-06 10:40:18 +0200362 self.assertEqual(21, pool['total_capacity_gb'])
363 self.assertEqual(5, int(pool['free_capacity_gb']))
364
365- self.assertTrue(pool['multiattach'])
366+ self.assertFalse(pool['multiattach'])
367 self.assertFalse(pool['QoS_support'])
368 self.assertFalse(pool['thick_provisioning_support'])
369 self.assertTrue(pool['thin_provisioning_support'])
Biser Milanov233be152025-01-24 17:35:11 +0200370@@ -690,3 +947,179 @@ class StorPoolTestCase(test.TestCase):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200371 'No such volume',
372 self.driver.revert_to_snapshot, None,
373 {'id': vol_id}, {'id': snap_id})
Peter Pentchev9c24be92022-09-26 22:35:24 +0300374+
375+ @ddt.data(
376+ # The default values
Biser Milanov233be152025-01-24 17:35:11 +0200377+ ('', False, constants.STORPOOL, ISCSI_IQN_OURS, False),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300378+
379+ # Export to all
Biser Milanov233be152025-01-24 17:35:11 +0200380+ ('*', True, constants.ISCSI, ISCSI_IQN_OURS, True),
381+ ('*', True, constants.ISCSI, ISCSI_IQN_OURS, True),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300382+
383+ # Only export to the controller
Biser Milanov233be152025-01-24 17:35:11 +0200384+ ('', False, constants.STORPOOL, ISCSI_IQN_OURS, False),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300385+
386+ # Some of the not-fully-supported pattern lists
Biser Milanov233be152025-01-24 17:35:11 +0200387+ (ISCSI_PAT_OTHER, False, constants.STORPOOL, ISCSI_IQN_OURS, False),
388+ (ISCSI_PAT_OTHER, False, constants.STORPOOL, ISCSI_IQN_OTHER, True),
389+ (ISCSI_PAT_BOTH, False, constants.STORPOOL, ISCSI_IQN_OURS, True),
390+ (ISCSI_PAT_BOTH, False, constants.STORPOOL, ISCSI_IQN_OTHER, True),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300391+ )
392+ @ddt.unpack
Biser Milanovd684c1c2024-11-22 12:12:59 +0200393+ def test_wants_iscsi(self, storpool_iscsi_export_to, use_iscsi,
394+ storage_protocol, hostname, expected):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300395+ """Check the "should this export use iSCSI?" detection."""
Biser Milanovd684c1c2024-11-22 12:12:59 +0200396+ self.cfg.storpool_iscsi_export_to = storpool_iscsi_export_to
Peter Pentchev9c24be92022-09-26 22:35:24 +0300397+ self._setup_test_driver()
398+ self.assertEqual(self.driver._use_iscsi, use_iscsi)
399+
400+ # Make sure the driver reports the correct protocol in the stats
401+ self.driver._update_volume_stats()
402+ self.assertEqual(self.driver._stats["vendor_name"], "StorPool")
403+ self.assertEqual(self.driver._stats["storage_protocol"],
404+ storage_protocol)
405+
406+ def check(conn, forced, expected):
407+ """Pass partially or completely valid connector info."""
408+ for initiator in (None, hostname):
Biser Milanov233be152025-01-24 17:35:11 +0200409+ for host in (None, ISCSI_IQN_THIRD):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300410+ self.assertEqual(
411+ self.driver._connector_wants_iscsi({
412+ "host": host,
413+ "initiator": initiator,
414+ **conn,
415+ }),
416+ expected if initiator is not None and host is not None
417+ else forced)
418+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200419+ # If storpool_iscsi_cinder_volume is set and this is the controller,
420+ # then yes.
Peter Pentchev9c24be92022-09-26 22:35:24 +0300421+ check({"storpool_wants_iscsi": True}, True, True)
422+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200423+ # If storpool_iscsi_cinder_volume is not set or this is not the
424+ # controller, then look at the specified expected value.
Peter Pentchev9c24be92022-09-26 22:35:24 +0300425+ check({"storpool_wants_iscsi": False}, use_iscsi, expected)
426+ check({}, use_iscsi, expected)
Peter Pentchevea354462023-07-18 11:15:56 +0300427+
428+ def _validate_iscsi_config(
429+ self,
Biser Milanov233be152025-01-24 17:35:11 +0200430+ cfg: dict,
Peter Pentchevea354462023-07-18 11:15:56 +0300431+ res: dict[str, Any],
432+ tcase: IscsiTestCase,
433+ ) -> None:
434+ """Make sure the returned structure makes sense."""
435+ initiator = res['initiator']
Biser Milanov233be152025-01-24 17:35:11 +0200436+ cfg_initiator = cfg['initiators'].get('1')
Peter Pentchevea354462023-07-18 11:15:56 +0300437+
Biser Milanov233be152025-01-24 17:35:11 +0200438+ self.assertIs(res['cfg']['iscsi'], cfg)
439+ self.assertEqual(res['pg']['name'], ISCSI_PORTAL_GROUP)
Peter Pentchevea354462023-07-18 11:15:56 +0300440+
441+ if tcase.initiator is None:
442+ self.assertIsNone(initiator)
443+ else:
444+ self.assertIsNotNone(initiator)
445+ self.assertEqual(initiator, cfg_initiator)
446+
447+ if tcase.volume is None:
448+ self.assertIsNone(res['target'])
449+ else:
450+ self.assertIsNotNone(res['target'])
Biser Milanov233be152025-01-24 17:35:11 +0200451+ self.assertEqual(res['target'], cfg['targets'].get('1'))
Peter Pentchevea354462023-07-18 11:15:56 +0300452+
453+ if tcase.initiator is None:
454+ self.assertIsNone(cfg_initiator)
455+ self.assertIsNone(res['export'])
456+ else:
457+ self.assertIsNotNone(cfg_initiator)
458+ if tcase.exported:
459+ self.assertIsNotNone(res['export'])
Biser Milanov233be152025-01-24 17:35:11 +0200460+ self.assertEqual(res['export'], cfg_initiator['exports'][0])
Peter Pentchevea354462023-07-18 11:15:56 +0300461+ else:
462+ self.assertIsNone(res['export'])
463+
464+ @ddt.data(*_ISCSI_TEST_CASES)
465+ def test_iscsi_get_config(self, tcase: IscsiTestCase) -> None:
466+ """Make sure the StorPool iSCSI configuration is parsed correctly."""
467+ cfg_orig = MockIscsiConfig.build(tcase)
468+ configs = [cfg_orig]
469+ iapi = MockIscsiAPI(configs, self)
Biser Milanov233be152025-01-24 17:35:11 +0200470+ with mock.patch.object(self.driver, '_sp_api', iapi):
Peter Pentchevea354462023-07-18 11:15:56 +0300471+ res = self.driver._get_iscsi_config(
Biser Milanov233be152025-01-24 17:35:11 +0200472+ ISCSI_IQN_OURS,
Biser Milanovec3bf982024-11-27 17:42:17 +0200473+ fake_constants.VOLUME_ID,
Peter Pentchevea354462023-07-18 11:15:56 +0300474+ )
475+
476+ self._validate_iscsi_config(cfg_orig, res, tcase)
477+
478+ @ddt.data(*_ISCSI_TEST_CASES)
479+ def test_iscsi_create_export(self, tcase: IscsiTestCase) -> None:
480+ """Make sure _create_iscsi_export() makes the right API calls."""
481+ cfg_orig = MockIscsiConfig.build(tcase)
482+ configs = [cfg_orig]
483+ iapi = MockIscsiAPI(configs, self)
Biser Milanov233be152025-01-24 17:35:11 +0200484+ with mock.patch.object(self.driver, '_sp_api', iapi):
Peter Pentchevea354462023-07-18 11:15:56 +0300485+ self.driver._create_iscsi_export(
486+ {
Biser Milanovec3bf982024-11-27 17:42:17 +0200487+ 'id': fake_constants.VOLUME_ID,
488+ 'display_name': fake_constants.VOLUME_NAME,
Peter Pentchevea354462023-07-18 11:15:56 +0300489+ },
490+ {
491+ # Yeah, okay, so we cheat a little bit here...
Biser Milanov233be152025-01-24 17:35:11 +0200492+ 'host': ISCSI_IQN_OURS + '.hostname',
493+ 'initiator': ISCSI_IQN_OURS,
Peter Pentchevea354462023-07-18 11:15:56 +0300494+ },
495+ )
496+
497+ self.assertEqual(len(configs), tcase.commands_count + 1)
498+ cfg_final = configs[-1]
Biser Milanov233be152025-01-24 17:35:11 +0200499+ self.assertEqual(cfg_final['initiators']['1']['name'], ISCSI_IQN_OURS)
Peter Pentchevea354462023-07-18 11:15:56 +0300500+ self.assertEqual(
Biser Milanov233be152025-01-24 17:35:11 +0200501+ cfg_final['initiators']['1']['exports'][0]['target'],
Biser Milanovec3bf982024-11-27 17:42:17 +0200502+ targetName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300503+ )
504+ self.assertEqual(
Biser Milanov233be152025-01-24 17:35:11 +0200505+ cfg_final['targets']['1']['volume'],
Biser Milanovec3bf982024-11-27 17:42:17 +0200506+ volumeName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300507+ )
Biser Milanovd684c1c2024-11-22 12:12:59 +0200508+
509+ @ddt.data(*_ISCSI_TEST_CASES)
510+ def test_remove_iscsi_export(self, tcase: IscsiTestCase):
511+ cfg_orig = MockIscsiConfig.build(tcase)
512+ configs = [cfg_orig]
513+ iapi = MockIscsiAPI(configs, self)
514+
Biser Milanov233be152025-01-24 17:35:11 +0200515+ def _target_exists(cfg: dict, volume: str) -> bool:
516+ for name, target in cfg['targets'].items():
517+ if target['volume'] == volumeName(volume):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200518+ return True
519+ return False
520+
Biser Milanov233be152025-01-24 17:35:11 +0200521+ def _export_exists(cfg: dict, volume: str) -> bool:
522+ for name, initiator in cfg['initiators'].items():
523+ for export in initiator['exports']:
524+ if export['target'] == targetName(volume):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200525+ return True
526+ return False
527+
528+ if tcase.exported:
529+ self.assertTrue(
Biser Milanov233be152025-01-24 17:35:11 +0200530+ _target_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Biser Milanovd684c1c2024-11-22 12:12:59 +0200531+ self.assertTrue(
Biser Milanov233be152025-01-24 17:35:11 +0200532+ _export_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Biser Milanovd684c1c2024-11-22 12:12:59 +0200533+
Biser Milanov233be152025-01-24 17:35:11 +0200534+ with mock.patch.object(self.driver, '_sp_api', iapi):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200535+ self.driver._remove_iscsi_export(
536+ {
Biser Milanovec3bf982024-11-27 17:42:17 +0200537+ 'id': fake_constants.VOLUME_ID,
538+ 'display_name': fake_constants.VOLUME_NAME,
Biser Milanovd684c1c2024-11-22 12:12:59 +0200539+ },
540+ {
Biser Milanov233be152025-01-24 17:35:11 +0200541+ 'host': ISCSI_IQN_OURS + '.hostname',
542+ 'initiator': ISCSI_IQN_OURS,
Biser Milanovd684c1c2024-11-22 12:12:59 +0200543+ },
544+ )
545+
546+ self.assertFalse(
Biser Milanov233be152025-01-24 17:35:11 +0200547+ _target_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Biser Milanovd684c1c2024-11-22 12:12:59 +0200548+ self.assertFalse(
Biser Milanov233be152025-01-24 17:35:11 +0200549+ _export_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Peter Pentchevacaaa382023-02-28 11:26:13 +0200550diff --git a/cinder/volume/drivers/storpool.py b/cinder/volume/drivers/storpool.py
Biser Milanov233be152025-01-24 17:35:11 +0200551index 2dc7bb6be..d9e607552 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +0300552--- a/cinder/volume/drivers/storpool.py
553+++ b/cinder/volume/drivers/storpool.py
Biser Milanovc0ee3582025-01-22 09:09:24 +0000554@@ -15,6 +15,7 @@
Peter Pentchev9c24be92022-09-26 22:35:24 +0300555
Biser Milanovda1b0682024-11-29 09:37:10 +0200556 """StorPool block device driver"""
557
Peter Pentchev9c24be92022-09-26 22:35:24 +0300558+import fnmatch
Biser Milanovda1b0682024-11-29 09:37:10 +0200559 import platform
Biser Milanov90b5a142024-11-28 11:33:39 +0200560
Biser Milanov233be152025-01-24 17:35:11 +0200561 from os_brick.initiator import storpool_utils
562@@ -36,6 +37,32 @@ LOG = logging.getLogger(__name__)
Peter Pentchev9c24be92022-09-26 22:35:24 +0300563
564
565 storpool_opts = [
Biser Milanovd684c1c2024-11-22 12:12:59 +0200566+ cfg.BoolOpt('storpool_iscsi_cinder_volume',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300567+ default=False,
568+ help='Let the cinder-volume service use iSCSI instead of '
569+ 'the StorPool block device driver for accessing '
570+ 'StorPool volumes, e.g. when creating a volume from '
571+ 'an image or vice versa.'),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200572+ cfg.StrOpt('storpool_iscsi_export_to',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300573+ default='',
574+ help='Whether to export volumes using iSCSI. '
575+ 'An empty string (the default) makes the driver export '
576+ 'all volumes using the StorPool native network protocol. '
577+ 'The value "*" makes the driver export all volumes using '
Biser Milanovd684c1c2024-11-22 12:12:59 +0200578+ 'iSCSI (see the Cinder StorPool driver documentation for '
579+ 'how this option and ``storpool_iscsi_cinder_volume`` '
580+ 'interact). Any other value leads to an experimental '
581+ 'not fully supported configuration and is interpreted as '
Peter Pentchev9c24be92022-09-26 22:35:24 +0300582+ 'a whitespace-separated list of patterns for IQNs for '
583+ 'hosts that need volumes to be exported via iSCSI, e.g. '
584+ '"iqn.1991-05.com.microsoft:\\*" for Windows hosts.'),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200585+ cfg.BoolOpt('storpool_iscsi_learn_initiator_iqns',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300586+ default=True,
587+ help='Create a StorPool record for a new initiator as soon as '
588+ 'Cinder asks for a volume to be exported to it.'),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200589+ cfg.StrOpt('storpool_iscsi_portal_group',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300590+ default=None,
591+ help='The portal group to export volumes via iSCSI in.'),
592 cfg.StrOpt('storpool_template',
593 default=None,
594 help='The StorPool template for volumes with no type.'),
Biser Milanov233be152025-01-24 17:35:11 +0200595@@ -89,10 +116,10 @@ class StorPoolDriver(driver.VolumeDriver):
596 2.1.0 - Use the new API client in os-brick to communicate with the
597 StorPool API instead of packages `storpool` and
598 `storpool.spopenstack`
599-
600+ 2.2.0 - Add iSCSI export support.
Biser Milanovd684c1c2024-11-22 12:12:59 +0200601 """
602
Biser Milanov233be152025-01-24 17:35:11 +0200603- VERSION = '2.1.0'
604+ VERSION = '2.2.0'
Biser Milanovd684c1c2024-11-22 12:12:59 +0200605 CI_WIKI_NAME = 'StorPool_distributed_storage_CI'
606
607 def __init__(self, *args, **kwargs):
Biser Milanov233be152025-01-24 17:35:11 +0200608@@ -103,6 +130,7 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300609 self._ourIdInt = None
Biser Milanov233be152025-01-24 17:35:11 +0200610 self._sp_api = None
611 self._volume_prefix = None
Biser Milanovd684c1c2024-11-22 12:12:59 +0200612+ self._use_iscsi = False
Peter Pentchev9c24be92022-09-26 22:35:24 +0300613
614 @staticmethod
615 def get_driver_options():
Biser Milanov233be152025-01-24 17:35:11 +0200616@@ -158,10 +186,322 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300617 raise StorPoolConfigurationInvalid(
618 section=hostname, param='SP_OURID', error=e)
619
620+ def _connector_wants_iscsi(self, connector):
621+ """Should we do this export via iSCSI?
622+
623+ Check the configuration to determine whether this connector is
624+ expected to provide iSCSI exports as opposed to native StorPool
625+ protocol ones. Match the initiator's IQN against the list of
Biser Milanovd684c1c2024-11-22 12:12:59 +0200626+ patterns supplied in the "storpool_iscsi_export_to" configuration
627+ setting.
Peter Pentchev9c24be92022-09-26 22:35:24 +0300628+ """
629+ if connector is None:
630+ return False
631+ if self._use_iscsi:
Biser Milanov233be152025-01-24 17:35:11 +0200632+ LOG.debug('forcing iSCSI for all exported volumes')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300633+ return True
634+ if connector.get('storpool_wants_iscsi'):
Biser Milanov233be152025-01-24 17:35:11 +0200635+ LOG.debug('forcing iSCSI for the controller')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300636+ return True
637+
Biser Milanov233be152025-01-24 17:35:11 +0200638+ iqn = connector.get('initiator', None)
639+ host = connector.get('host', None)
640+
Peter Pentchev9c24be92022-09-26 22:35:24 +0300641+ if iqn is None or host is None:
Biser Milanov233be152025-01-24 17:35:11 +0200642+ LOG.debug('this connector certainly does not want iSCSI')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300643+ return False
644+
Biser Milanov233be152025-01-24 17:35:11 +0200645+ LOG.debug('check whether %(host)s (%(iqn)s) wants iSCSI',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300646+ {
647+ 'host': host,
648+ 'iqn': iqn,
649+ })
650+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200651+ export_to = self.configuration.storpool_iscsi_export_to
Peter Pentchev9c24be92022-09-26 22:35:24 +0300652+ if export_to is None:
653+ return False
654+
655+ for pat in export_to.split():
Biser Milanov233be152025-01-24 17:35:11 +0200656+ LOG.debug('matching against %(pat)s', {'pat': pat})
Peter Pentchev9c24be92022-09-26 22:35:24 +0300657+ if fnmatch.fnmatch(iqn, pat):
Biser Milanov233be152025-01-24 17:35:11 +0200658+ LOG.debug('got it!')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300659+ return True
Biser Milanov233be152025-01-24 17:35:11 +0200660+ LOG.debug('nope')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300661+ return False
662+
663 def validate_connector(self, connector):
664+ if self._connector_wants_iscsi(connector):
665+ return True
666 return self._storpool_client_id(connector) >= 0
667
668+ def _get_iscsi_config(self, iqn, volume_id):
669+ """Get the StorPool iSCSI config items pertaining to this volume.
670+
671+ Find the elements of the StorPool iSCSI configuration tree that
672+ will be needed to create, ensure, or remove the iSCSI export of
673+ the specified volume to the specified initiator.
674+ """
Biser Milanov233be152025-01-24 17:35:11 +0200675+ cfg = self._sp_api.get_iscsi_config()
Peter Pentchev9c24be92022-09-26 22:35:24 +0300676+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200677+ pg_name = self.configuration.storpool_iscsi_portal_group
Peter Pentchev9c24be92022-09-26 22:35:24 +0300678+ pg_found = [
Biser Milanov233be152025-01-24 17:35:11 +0200679+ pg for pg in cfg['iscsi']['portalGroups'].values() if pg['name'] == pg_name
Peter Pentchev9c24be92022-09-26 22:35:24 +0300680+ ]
681+ if not pg_found:
682+ raise Exception('StorPool Cinder iSCSI configuration error: '
683+ 'no portal group "{pg}"'.format(pg=pg_name))
684+ pg = pg_found[0]
685+
686+ # Do we know about this initiator?
687+ i_found = [
Biser Milanov233be152025-01-24 17:35:11 +0200688+ init for init in cfg['iscsi']['initiators'].values() if init['name'] == iqn
Peter Pentchev9c24be92022-09-26 22:35:24 +0300689+ ]
690+ if i_found:
691+ initiator = i_found[0]
692+ else:
693+ initiator = None
694+
695+ # Is this volume already being exported?
Biser Milanov233be152025-01-24 17:35:11 +0200696+ volname = storpool_utils.os_to_sp_volume_name(volume_id)
Peter Pentchev9c24be92022-09-26 22:35:24 +0300697+ t_found = [
Biser Milanov233be152025-01-24 17:35:11 +0200698+ tgt for tgt in cfg['iscsi']['targets'].values() if tgt['volume'] == volname
Peter Pentchev9c24be92022-09-26 22:35:24 +0300699+ ]
700+ if t_found:
701+ target = t_found[0]
702+ else:
703+ target = None
704+
705+ # OK, so is this volume being exported to this initiator?
706+ export = None
707+ if initiator is not None and target is not None:
708+ e_found = [
Biser Milanov233be152025-01-24 17:35:11 +0200709+ exp for exp in initiator['exports']
710+ if exp['portalGroup'] == pg['name'] and exp['target'] == target['name']
Peter Pentchev9c24be92022-09-26 22:35:24 +0300711+ ]
712+ if e_found:
713+ export = e_found[0]
714+
715+ return {
716+ 'cfg': cfg,
717+ 'pg': pg,
718+ 'initiator': initiator,
719+ 'target': target,
720+ 'export': export,
721+ 'volume_name': volname,
722+ 'volume_id': volume_id,
723+ }
724+
725+ def _create_iscsi_export(self, volume, connector):
726+ """Create (if needed) an iSCSI export for the StorPool volume."""
727+ LOG.debug(
728+ '_create_iscsi_export() invoked for volume '
729+ '"%(vol_name)s" (%(vol_id)s) connector %(connector)s',
730+ {
731+ 'vol_name': volume['display_name'],
732+ 'vol_id': volume['id'],
733+ 'connector': connector,
734+ }
735+ )
736+ iqn = connector['initiator']
737+ try:
738+ cfg = self._get_iscsi_config(iqn, volume['id'])
739+ except Exception as exc:
740+ LOG.error(
741+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
742+ )
743+ raise
744+
745+ if cfg['initiator'] is None:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200746+ if not (self.configuration.storpool_iscsi_learn_initiator_iqns or
747+ self.configuration.storpool_iscsi_cinder_volume and
Peter Pentchev9c24be92022-09-26 22:35:24 +0300748+ connector.get('storpool_wants_iscsi')):
749+ raise Exception('The "{iqn}" initiator IQN for the "{host}" '
750+ 'host is not defined in the StorPool '
751+ 'configuration.'
752+ .format(iqn=iqn, host=connector['host']))
753+ else:
754+ LOG.info('Creating a StorPool iSCSI initiator '
755+ 'for "{host}s" ({iqn}s)',
756+ {'host': connector['host'], 'iqn': iqn})
Biser Milanov233be152025-01-24 17:35:11 +0200757+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300758+ 'commands': [
759+ {
760+ 'createInitiator': {
761+ 'name': iqn,
762+ 'username': '',
763+ 'secret': '',
764+ },
765+ },
766+ {
767+ 'initiatorAddNetwork': {
768+ 'initiator': iqn,
769+ 'net': '0.0.0.0/0',
770+ },
771+ },
772+ ]
773+ })
774+
775+ if cfg['target'] is None:
776+ LOG.info(
777+ 'Creating a StorPool iSCSI target '
778+ 'for the "%(vol_name)s" volume (%(vol_id)s)',
779+ {
780+ 'vol_name': volume['display_name'],
781+ 'vol_id': volume['id'],
782+ }
783+ )
Biser Milanov233be152025-01-24 17:35:11 +0200784+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300785+ 'commands': [
786+ {
787+ 'createTarget': {
788+ 'volumeName': cfg['volume_name'],
789+ },
790+ },
791+ ]
792+ })
793+ cfg = self._get_iscsi_config(iqn, volume['id'])
794+
795+ if cfg['export'] is None:
796+ LOG.info('Creating a StorPool iSCSI export '
797+ 'for the "{vol_name}s" volume ({vol_id}s) '
798+ 'to the "{host}s" initiator ({iqn}s) '
799+ 'in the "{pg}s" portal group',
800+ {
801+ 'vol_name': volume['display_name'],
802+ 'vol_id': volume['id'],
803+ 'host': connector['host'],
804+ 'iqn': iqn,
Biser Milanov233be152025-01-24 17:35:11 +0200805+ 'pg': cfg['pg']['name']
Peter Pentchev9c24be92022-09-26 22:35:24 +0300806+ })
Biser Milanov233be152025-01-24 17:35:11 +0200807+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300808+ 'commands': [
809+ {
810+ 'export': {
811+ 'initiator': iqn,
Biser Milanov233be152025-01-24 17:35:11 +0200812+ 'portalGroup': cfg['pg']['name'],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300813+ 'volumeName': cfg['volume_name'],
814+ },
815+ },
816+ ]
817+ })
818+
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200819+ target_portals = [
Biser Milanov233be152025-01-24 17:35:11 +0200820+ "{addr}:3260".format(addr=net['address'])
821+ for net in cfg['pg']['networks']
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200822+ ]
Biser Milanov233be152025-01-24 17:35:11 +0200823+ target_iqns = [cfg['target']['name']] * len(target_portals)
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200824+ target_luns = [0] * len(target_portals)
825+ if connector.get('multipath', False):
826+ multipath_settings = {
827+ 'target_iqns': target_iqns,
828+ 'target_portals': target_portals,
829+ 'target_luns': target_luns,
830+ }
831+ else:
832+ multipath_settings = {}
833+
Peter Pentchev9c24be92022-09-26 22:35:24 +0300834+ res = {
835+ 'driver_volume_type': 'iscsi',
836+ 'data': {
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200837+ **multipath_settings,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300838+ 'target_discovered': False,
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200839+ 'target_iqn': target_iqns[0],
840+ 'target_portal': target_portals[0],
841+ 'target_lun': target_luns[0],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300842+ 'volume_id': volume['id'],
843+ 'discard': True,
844+ },
845+ }
846+ LOG.debug('returning %(res)s', {'res': res})
847+ return res
848+
849+ def _remove_iscsi_export(self, volume, connector):
850+ """Remove an iSCSI export for the specified StorPool volume."""
851+ LOG.debug(
852+ '_remove_iscsi_export() invoked for volume '
853+ '"%(vol_name)s" (%(vol_id)s) connector %(conn)s',
854+ {
855+ 'vol_name': volume['display_name'],
856+ 'vol_id': volume['id'],
857+ 'conn': connector,
858+ }
859+ )
860+ try:
861+ cfg = self._get_iscsi_config(connector['initiator'], volume['id'])
862+ except Exception as exc:
863+ LOG.error(
864+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
865+ )
866+ raise
867+
868+ if cfg['export'] is not None:
869+ LOG.info('Removing the StorPool iSCSI export '
870+ 'for the "%(vol_name)s" volume (%(vol_id)s) '
871+ 'to the "%(host)s" initiator (%(iqn)s) '
872+ 'in the "%(pg)s" portal group',
873+ {
874+ 'vol_name': volume['display_name'],
875+ 'vol_id': volume['id'],
876+ 'host': connector['host'],
877+ 'iqn': connector['initiator'],
Biser Milanov233be152025-01-24 17:35:11 +0200878+ 'pg': cfg['pg']['name'],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300879+ })
880+ try:
Biser Milanov233be152025-01-24 17:35:11 +0200881+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300882+ 'commands': [
883+ {
884+ 'exportDelete': {
Biser Milanov233be152025-01-24 17:35:11 +0200885+ 'initiator': cfg['initiator']['name'],
886+ 'portalGroup': cfg['pg']['name'],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300887+ 'volumeName': cfg['volume_name'],
888+ },
889+ },
890+ ]
891+ })
Biser Milanov233be152025-01-24 17:35:11 +0200892+ except storpool_utils.StorPoolAPIError as e:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300893+ if e.name not in ('objectExists', 'objectDoesNotExist'):
894+ raise
895+ LOG.info('Looks like somebody beat us to it')
896+
897+ if cfg['target'] is not None:
898+ last = True
Biser Milanov233be152025-01-24 17:35:11 +0200899+ for initiator in cfg['cfg']['iscsi']['initiators'].values():
900+ if initiator['name'] == cfg['initiator']['name']:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300901+ continue
Biser Milanov233be152025-01-24 17:35:11 +0200902+ for exp in initiator['exports']:
903+ if exp['target'] == cfg['target']['name']:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300904+ last = False
905+ break
906+ if not last:
907+ break
908+
909+ if last:
910+ LOG.info(
911+ 'Removing the StorPool iSCSI target '
912+ 'for the "{vol_name}s" volume ({vol_id}s)',
913+ {
914+ 'vol_name': volume['display_name'],
915+ 'vol_id': volume['id'],
916+ }
917+ )
918+ try:
Biser Milanov233be152025-01-24 17:35:11 +0200919+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300920+ 'commands': [
921+ {
922+ 'deleteTarget': {
923+ 'volumeName': cfg['volume_name'],
924+ },
925+ },
926+ ]
927+ })
Biser Milanov233be152025-01-24 17:35:11 +0200928+ except storpool_utils.StorPoolAPIError as e:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300929+ if e.name not in ('objectDoesNotExist', 'invalidParam'):
930+ raise
931+ LOG.info('Looks like somebody beat us to it')
932+
933 def initialize_connection(self, volume, connector):
934+ if self._connector_wants_iscsi(connector):
935+ return self._create_iscsi_export(volume, connector)
936 return {'driver_volume_type': 'storpool',
937 'data': {
938 'client_id': self._storpool_client_id(connector),
Biser Milanov233be152025-01-24 17:35:11 +0200939@@ -170,6 +510,9 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300940 }}
941
942 def terminate_connection(self, volume, connector, **kwargs):
943+ if self._connector_wants_iscsi(connector):
Biser Milanov233be152025-01-24 17:35:11 +0200944+ LOG.debug('removing an iSCSI export')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300945+ self._remove_iscsi_export(volume, connector)
946 pass
947
948 def create_snapshot(self, snapshot):
Biser Milanov233be152025-01-24 17:35:11 +0200949@@ -278,11 +621,20 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300950 )
951
952 def create_export(self, context, volume, connector):
953- pass
954+ if self._connector_wants_iscsi(connector):
Biser Milanov233be152025-01-24 17:35:11 +0200955+ LOG.debug('creating an iSCSI export')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300956+ self._create_iscsi_export(volume, connector)
957
958 def remove_export(self, context, volume):
959 pass
960
961+ def _attach_volume(self, context, volume, properties, remote=False):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200962+ if self.configuration.storpool_iscsi_cinder_volume and not remote:
Biser Milanov233be152025-01-24 17:35:11 +0200963+ LOG.debug('adding the "storpool_wants_iscsi" flag')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300964+ properties['storpool_wants_iscsi'] = True
965+
966+ return super()._attach_volume(context, volume, properties, remote)
967+
968 def delete_volume(self, volume):
Biser Milanov233be152025-01-24 17:35:11 +0200969 name = storpool_utils.os_to_sp_volume_name(
970 self._volume_prefix, volume['id'])
971@@ -321,6 +673,17 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300972 LOG.error("StorPoolDriver API initialization failed: %s", e)
973 raise
974
Biser Milanovd684c1c2024-11-22 12:12:59 +0200975+ export_to = self.configuration.storpool_iscsi_export_to
Peter Pentchev9c24be92022-09-26 22:35:24 +0300976+ export_to_set = export_to is not None and export_to.split()
Biser Milanovd684c1c2024-11-22 12:12:59 +0200977+ vol_iscsi = self.configuration.storpool_iscsi_cinder_volume
978+ pg_name = self.configuration.storpool_iscsi_portal_group
Peter Pentchev9c24be92022-09-26 22:35:24 +0300979+ if (export_to_set or vol_iscsi) and pg_name is None:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200980+ msg = _('The "storpool_iscsi_portal_group" option is required if '
981+ 'any patterns are listed in "storpool_iscsi_export_to"')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300982+ raise exception.VolumeDriverException(message=msg)
983+
984+ self._use_iscsi = export_to == "*"
985+
986 def _update_volume_stats(self):
987 try:
Biser Milanov233be152025-01-24 17:35:11 +0200988 dl = self._sp_api.disks_list()
989@@ -346,7 +709,7 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300990 'total_capacity_gb': total / units.Gi,
991 'free_capacity_gb': free / units.Gi,
992 'reserved_percentage': 0,
993- 'multiattach': True,
Peter Pentchev5a9f8a62023-12-06 10:40:18 +0200994+ 'multiattach': self._use_iscsi,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300995 'QoS_support': False,
996 'thick_provisioning_support': False,
997 'thin_provisioning_support': True,
Biser Milanov233be152025-01-24 17:35:11 +0200998@@ -365,7 +728,9 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300999 'volume_backend_name') or 'storpool',
1000 'vendor_name': 'StorPool',
1001 'driver_version': self.VERSION,
1002- 'storage_protocol': constants.STORPOOL,
1003+ 'storage_protocol': (
1004+ constants.ISCSI if self._use_iscsi else constants.STORPOOL
1005+ ),
Peter Pentchevacaaa382023-02-28 11:26:13 +02001006 # Driver capabilities
Peter Pentchev9c24be92022-09-26 22:35:24 +03001007 'clone_across_pools': True,
1008 'sparse_copy_volume': True,
Biser Milanov233be152025-01-24 17:35:11 +02001009@@ -452,15 +817,15 @@ class StorPoolDriver(driver.VolumeDriver):
1010 LOG.debug('Trying to swap volume names, intermediate "%(int)s"',
1011 {'int': int_name})
1012 try:
1013- LOG.debug('- rename "%(orig)s" to "%(int)s"',
1014+ LOG.debug('rename "%(orig)s" to "%(int)s"',
1015 {'orig': orig_name, 'int': int_name})
1016 self._sp_api.volume_update(orig_name, {'rename': int_name})
1017
1018- LOG.debug('- rename "%(temp)s" to "%(orig)s"',
1019+ LOG.debug('rename "%(temp)s" to "%(orig)s"',
1020 {'temp': temp_name, 'orig': orig_name})
1021 self._sp_api.volume_update(temp_name, {'rename': orig_name})
1022
1023- LOG.debug('- rename "%(int)s" to "%(temp)s"',
1024+ LOG.debug('rename "%(int)s" to "%(temp)s"',
1025 {'int': int_name, 'temp': temp_name})
1026 self._sp_api.volume_update(int_name, {'rename': temp_name})
1027 return {'_name_id': None}
Peter Pentchevacaaa382023-02-28 11:26:13 +02001028diff --git a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
Biser Milanovc0ee3582025-01-22 09:09:24 +00001029index d2c5895a9..936e83675 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +03001030--- a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
1031+++ b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
Peter Pentchevacaaa382023-02-28 11:26:13 +02001032@@ -19,12 +19,15 @@ Prerequisites
Peter Pentchev9c24be92022-09-26 22:35:24 +03001033 * The controller and all the compute nodes must have access to the StorPool
1034 API service.
1035
1036-* All nodes where StorPool-backed volumes will be attached must have access to
1037+* If iSCSI is not being used as a transport (see below), all nodes where
1038+ StorPool-backed volumes will be attached must have access to
1039 the StorPool data network and run the ``storpool_block`` service.
1040
1041-* If StorPool-backed Cinder volumes need to be created directly from Glance
1042- images, then the node running the ``cinder-volume`` service must also have
1043- access to the StorPool data network and run the ``storpool_block`` service.
1044+* If Glance uses Cinder as its image store, or if StorPool-backed Cinder
1045+ volumes need to be created directly from Glance images, and iSCSI is not
1046+ being used as a transport, then the node running the ``cinder-volume``
1047+ service must also have access to the StorPool data network and run
1048+ the ``storpool_block`` service.
1049
1050 * All nodes that need to access the StorPool API (the compute nodes and
1051 the node running the ``cinder-volume`` service) must have the following
Biser Milanovc0ee3582025-01-22 09:09:24 +00001052@@ -34,6 +37,34 @@ Prerequisites
Peter Pentchev9c24be92022-09-26 22:35:24 +03001053 * the storpool Python bindings package
1054 * the storpool.spopenstack Python helper package
1055
1056+Using iSCSI as the transport protocol
1057+-------------------------------------
1058+
1059+The StorPool distributed storage system uses its own, highly optimized and
1060+tailored for its specifics, network protocol for communication between
1061+the storage servers and the clients (the OpenStack cluster nodes where
1062+StorPool-backed volumes will be attached). There are cases when granting
1063+various nodes access to the StorPool data network or installing and
1064+running the ``storpool_block`` client service on them may pose difficulties.
1065+The StorPool servers may also expose the user-created volumes and snapshots
1066+using the standard iSCSI protocol that only requires TCP routing and
1067+connectivity between the storage servers and the StorPool clients.
1068+The StorPool Cinder driver may be configured to export volumes and
Biser Milanovc0ee3582025-01-22 09:09:24 +00001069+snapshots via iSCSI using the ``storpool_iscsi_export_to`` and
1070+``storpool_iscsi_portal_group`` configuration options.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001071+
1072+Additionally, even if e.g. the hypervisor nodes running Nova will use
1073+the StorPool network protocol and run the ``storpool_block`` service
Biser Milanovc0ee3582025-01-22 09:09:24 +00001074+(so the ``storpool_iscsi_export_to`` option has its default empty string
1075+value), the ``storpool_iscsi_cinder_volume`` option configures the
1076+StorPool Cinder driver so that only the ``cinder-volume`` service will
1077+use the iSCSI protocol when attaching volumes and snapshots to transfer
1078+data to and from Glance images.
Biser Milanovd684c1c2024-11-22 12:12:59 +02001079+
1080+Multiattach support for StorPool is only enabled if iSCSI is used:
Biser Milanovc0ee3582025-01-22 09:09:24 +00001081+``storpool_iscsi_export_to`` is set to ``*``, that is, when all StorPool
1082+volumes will be exported via iSCSI to all initiators.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001083+
1084 Configuring the StorPool volume driver
1085 --------------------------------------
1086
Biser Milanovc0ee3582025-01-22 09:09:24 +00001087@@ -55,6 +86,35 @@ volume backend definition) and per volume type:
Peter Pentchev9c24be92022-09-26 22:35:24 +03001088 with the default placement constraints for the StorPool cluster.
1089 The default value for the chain replication is 3.
1090
Peter Pentchevea354462023-07-18 11:15:56 +03001091+In addition, if the iSCSI protocol is used to access the StorPool cluster as
1092+described in the previous section, the following options may be defined in
1093+the ``cinder.conf`` volume backend definition:
1094+
Biser Milanovc0ee3582025-01-22 09:09:24 +00001095+- ``storpool_iscsi_export_to``: if set to the value ``*``, the StorPool
1096+ Cinder driver will export volumes and snapshots using the iSCSI
1097+ protocol instead of the StorPool network protocol. The
1098+ ``storpool_iscsi_portal_group`` option must also be specified.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001099+
Biser Milanovc0ee3582025-01-22 09:09:24 +00001100+- ``storpool_iscsi_portal_group``: if the ``storpool_iscsi_export_to``
1101+ option is set to the value ``*`` or the
1102+ ``storpool_iscsi_cinder_volume`` option is turned on, this option
1103+ specifies the name of the iSCSI portal group that Cinder volumes will
1104+ be exported to.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001105+
Biser Milanovc0ee3582025-01-22 09:09:24 +00001106+- ``storpool_iscsi_cinder_volume``: if enabled, even if the
1107+ ``storpool_iscsi_export_to`` option has its default empty value, the
1108+ ``cinder-volume`` service will use iSCSI to attach the volumes and
1109+ snapshots for transferring data to and from Glance images if Glance is
1110+ configured to use the Cinder glance_store.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001111+
Biser Milanovc0ee3582025-01-22 09:09:24 +00001112+- ``storpool_iscsi_learn_initiator_iqns``: if enabled, the StorPool
1113+ Cinder driver will automatically use the StorPool API to create
1114+ definitions for new initiators in the StorPool cluster's
1115+ configuration. This is the default behavior of the driver; it may be
1116+ disabled in the rare case if, e.g. because of site policy, OpenStack
1117+ iSCSI initiators (e.g. Nova hypervisors) need to be explicitly allowed
1118+ to use the StorPool iSCSI targets.
Peter Pentchevea354462023-07-18 11:15:56 +03001119+
Peter Pentchev9c24be92022-09-26 22:35:24 +03001120 Using the StorPool volume driver
1121 --------------------------------
1122
Biser Milanovc0ee3582025-01-22 09:09:24 +00001123diff --git a/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml b/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
1124new file mode 100644
1125index 000000000..3863e4099
1126--- /dev/null
1127+++ b/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
1128@@ -0,0 +1,15 @@
1129+features:
1130+ - |
1131+ StorPool driver: Added support for exporting the StorPool-backed
1132+ volumes using the iSCSI protocol, so that the Cinder volume service
1133+ and/or the Nova or Glance consumers do not need to have the StorPool
1134+ block device third-party service installed. See the StorPool driver
1135+ section in the Cinder documentation for more information on the
1136+ ``storpool_iscsi_export_to``, ``storpool_iscsi_portal_group``,
1137+ ``storpool_iscsi_cinder_volume``, and
1138+ ``storpool_iscsi_learn_initiator_iqns`` options.
1139+
1140+ .. note::
1141+ Multiattach support for StorPool is now only enabled if
1142+ ``storpool_iscsi_export_to`` is set to ``*``, that is, when all
1143+ StorPool volumes will be exported via iSCSI to all initiators.
1144--
11452.43.0
1146