blob: 2c5be36b886feb8aa42c7de74dab71959897ed39 [file] [log] [blame]
Biser Milanov63fec2e2024-11-05 14:20:57 +02001From 0ae51594a69277e95bae758f9a7ec39de43af365 Mon Sep 17 00:00:00 2001
Peter Pentchev9c24be92022-09-26 22:35:24 +03002From: Peter Penchev <openstack-dev@storpool.com>
Biser Milanov63fec2e2024-11-05 14:20:57 +02003Date: Tue, 5 Nov 2024 11:28:47 +0200
Peter Pentchev5cf673c2024-02-20 10:04:41 +02004Subject: [PATCH 9/9] Add iSCSI export support to the StorPool driver
Peter Pentchev9c24be92022-09-26 22:35:24 +03005
6Add four new driver options:
7- iscsi_cinder_volume: use StorPool iSCSI attachments whenever
8 the cinder-volume service needs to attach a volume to the controller,
9 e.g. for copying an image to a volume or vice versa
10- iscsi_export_to:
11 - an empty string to use the StorPool native protocol for exporting volumes
12 protocol for exporting volumes)
13 - the string "*" to always use iSCSI for exporting volumes
14 - an experimental, not fully supported list of IQN patterns to export
15 volumes to using iSCSI; this results in a Cinder driver that exports
16 different volumes using different storage protocols
17- iscsi_portal_group: the name of the iSCSI portal group defined in
18 the StorPool configuration to use for these export
19- iscsi_learn_initiator_iqns: automatically create StorPool configuration
20 records for an initiator when a volume is first exported to it
21
22When exporting volumes via iSCSI, report the storage protocol as "iSCSI" and
23disable multiattach (the StorPool CI failures with iSCSI multiattach may need
24further investigation).
25
26Change-Id: I9de64306e0e6976268df782053b0651dd1cca96f
27---
Biser Milanov63fec2e2024-11-05 14:20:57 +020028 .../unit/volume/drivers/test_storpool.py | 438 +++++++++++++++++-
29 cinder/volume/drivers/storpool.py | 408 +++++++++++++++-
30 .../drivers/storpool-volume-driver.rst | 64 ++-
31 3 files changed, 899 insertions(+), 11 deletions(-)
Peter Pentchev9c24be92022-09-26 22:35:24 +030032
Peter Pentchevacaaa382023-02-28 11:26:13 +020033diff --git a/cinder/tests/unit/volume/drivers/test_storpool.py b/cinder/tests/unit/volume/drivers/test_storpool.py
Biser Milanov63fec2e2024-11-05 14:20:57 +020034index 60a351054..7d0d149bf 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +030035--- a/cinder/tests/unit/volume/drivers/test_storpool.py
36+++ b/cinder/tests/unit/volume/drivers/test_storpool.py
Biser Milanov63fec2e2024-11-05 14:20:57 +020037@@ -15,15 +15,19 @@
Peter Pentchevea354462023-07-18 11:15:56 +030038
39
Biser Milanov63fec2e2024-11-05 14:20:57 +020040 import copy
Peter Pentchevea354462023-07-18 11:15:56 +030041+import dataclasses
42 import itertools
Biser Milanov63fec2e2024-11-05 14:20:57 +020043 import os
Peter Pentchevea354462023-07-18 11:15:56 +030044 import re
Peter Pentchevea354462023-07-18 11:15:56 +030045+from typing import Any, NamedTuple, TYPE_CHECKING # noqa: H301
46 from unittest import mock
47
48 import ddt
49 from oslo_utils import units
Peter Pentchevea354462023-07-18 11:15:56 +030050
Peter Pentchev9c24be92022-09-26 22:35:24 +030051+from cinder.common import constants
52 from cinder import exception
Peter Pentchevea354462023-07-18 11:15:56 +030053+from cinder.tests.unit import fake_constants as fconst
Peter Pentchev9c24be92022-09-26 22:35:24 +030054 from cinder.tests.unit import test
55 from cinder.volume import configuration as conf
Peter Pentchevea354462023-07-18 11:15:56 +030056 from cinder.volume.drivers import storpool as driver
Biser Milanov63fec2e2024-11-05 14:20:57 +020057@@ -55,6 +59,12 @@ SP_CONF = {
58 'SP_OURID': '1'
59 }
Peter Pentchevea354462023-07-18 11:15:56 +030060
61+_ISCSI_IQN_OURS = 'beleriand'
62+_ISCSI_IQN_OTHER = 'rohan'
63+_ISCSI_IQN_THIRD = 'gondor'
64+_ISCSI_PAT_OTHER = 'roh*'
65+_ISCSI_PAT_BOTH = '*riand roh*'
66+_ISCSI_PORTAL_GROUP = 'openstack_pg'
Biser Milanov63fec2e2024-11-05 14:20:57 +020067
Peter Pentchevea354462023-07-18 11:15:56 +030068 volume_types = {
69 1: {},
Biser Milanov63fec2e2024-11-05 14:20:57 +020070@@ -93,6 +103,10 @@ def snapshotName(vtype, vid, more=None):
71 )
Peter Pentchevea354462023-07-18 11:15:56 +030072
73
74+def targetName(vid):
75+ return 'iqn.2012-11.storpool:{id}'.format(id=vid)
76+
77+
Biser Milanov63fec2e2024-11-05 14:20:57 +020078 class MockAPI(object):
79 def __init__(self, *args):
80 self._disks = {}
81@@ -203,6 +217,241 @@ class MockVolumeDB(object):
82 'volume_type': self.vol_types.get(vid),
83 }
Peter Pentchevea354462023-07-18 11:15:56 +030084
Peter Pentchevea354462023-07-18 11:15:56 +030085+class IscsiTestCase(NamedTuple):
86+ """A single test case for the iSCSI config and export methods."""
87+
88+ initiator: str | None
89+ volume: str | None
90+ exported: bool
91+ commands_count: int
92+
93+
94+@dataclasses.dataclass(frozen=True)
95+class MockIscsiConfig:
96+ """Mock the structure returned by the "get current config" query."""
97+
Peter Pentchevea354462023-07-18 11:15:56 +030098+ @classmethod
Biser Milanov63fec2e2024-11-05 14:20:57 +020099+ def build(cls, tcase: IscsiTestCase) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300100+ """Build a test config structure."""
101+ initiators = {
Biser Milanov63fec2e2024-11-05 14:20:57 +0200102+ '0': {'name': _ISCSI_IQN_OTHER, 'exports': []},
Peter Pentchevea354462023-07-18 11:15:56 +0300103+ }
104+ if tcase.initiator is not None:
Biser Milanov63fec2e2024-11-05 14:20:57 +0200105+ initiators['1'] = {
106+ 'name': tcase.initiator,
107+ 'exports': (
Peter Pentchevea354462023-07-18 11:15:56 +0300108+ [
Biser Milanov63fec2e2024-11-05 14:20:57 +0200109+ {
110+ 'portalGroup': _ISCSI_PORTAL_GROUP,
111+ 'target': targetName(tcase.volume),
112+ },
Peter Pentchevea354462023-07-18 11:15:56 +0300113+ ]
114+ if tcase.exported
115+ else []
116+ ),
Biser Milanov63fec2e2024-11-05 14:20:57 +0200117+ }
Peter Pentchevea354462023-07-18 11:15:56 +0300118+
119+ targets = {
Biser Milanov63fec2e2024-11-05 14:20:57 +0200120+ '0': {
121+ 'name': targetName(fconst.VOLUME2_ID),
122+ 'volume': volumeName(fconst.VOLUME2_ID),
123+ },
Peter Pentchevea354462023-07-18 11:15:56 +0300124+ }
125+ if tcase.volume is not None:
Biser Milanov63fec2e2024-11-05 14:20:57 +0200126+ targets['1'] = {
127+ 'name': targetName(tcase.volume),
128+ 'volume': volumeName(tcase.volume),
129+ }
Peter Pentchevea354462023-07-18 11:15:56 +0300130+
Biser Milanov63fec2e2024-11-05 14:20:57 +0200131+ return {
132+
133+ 'portalGroups': {
134+ '0': {
135+ 'name': _ISCSI_PORTAL_GROUP + '-not',
136+ 'networks': [],
137+ },
138+ '1': {
139+ 'name': _ISCSI_PORTAL_GROUP,
140+ 'networks': [
141+ {'address': "192.0.2.0"},
142+ {'address': "195.51.100.0"},
Peter Pentchevea354462023-07-18 11:15:56 +0300143+ ],
Biser Milanov63fec2e2024-11-05 14:20:57 +0200144+ },
Peter Pentchevea354462023-07-18 11:15:56 +0300145+ },
Biser Milanov63fec2e2024-11-05 14:20:57 +0200146+ 'initiators': initiators,
147+ 'targets': targets,
Peter Pentchevea354462023-07-18 11:15:56 +0300148+
Biser Milanov63fec2e2024-11-05 14:20:57 +0200149+ }
Peter Pentchevea354462023-07-18 11:15:56 +0300150+
Peter Pentchevea354462023-07-18 11:15:56 +0300151+
152+
153+class MockIscsiAPI:
154+ """Mock only the iSCSI-related calls of the StorPool API bindings."""
155+
156+ _asrt: test.TestCase
Biser Milanov63fec2e2024-11-05 14:20:57 +0200157+ _configs: list[dict]
Peter Pentchevea354462023-07-18 11:15:56 +0300158+
159+ def __init__(
160+ self,
Biser Milanov63fec2e2024-11-05 14:20:57 +0200161+ configs: list[dict],
Peter Pentchevea354462023-07-18 11:15:56 +0300162+ asrt: test.TestCase,
163+ ) -> None:
164+ """Store the reference to the list of iSCSI config objects."""
165+ self._asrt = asrt
166+ self._configs = configs
167+
Biser Milanov63fec2e2024-11-05 14:20:57 +0200168+ def get_iscsi_config(self) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300169+ """Return the last version of the iSCSI configuration."""
Biser Milanov63fec2e2024-11-05 14:20:57 +0200170+ return {'iscsi': self._configs[-1]}
Peter Pentchevea354462023-07-18 11:15:56 +0300171+
Biser Milanov63fec2e2024-11-05 14:20:57 +0200172+ def _handle_export(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300173+ """Add an export for an initiator."""
174+ self._asrt.assertDictEqual(
175+ cmd,
176+ {
177+ 'initiator': _ISCSI_IQN_OURS,
178+ 'portalGroup': _ISCSI_PORTAL_GROUP,
179+ 'volumeName': volumeName(fconst.VOLUME_ID),
180+ },
181+ )
Biser Milanov63fec2e2024-11-05 14:20:57 +0200182+ self._asrt.assertEqual(cfg['initiators']['1']['name'], cmd['initiator'])
183+ self._asrt.assertListEqual(cfg['initiators']['1']['exports'], [])
Peter Pentchevea354462023-07-18 11:15:56 +0300184+
Biser Milanov63fec2e2024-11-05 14:20:57 +0200185+ cfg['initiators'] = {
186+ **cfg['initiators'],
187+ '1': {
188+ **cfg['initiators']['1'],
189+ 'exports': [
190+ {
191+ 'portalGroup': cmd['portalGroup'],
192+ 'target': targetName(fconst.VOLUME_ID),
193+ },
194+ ],
195+ },
196+ }
197+ return cfg
198+
199+ def _handle_delete_export(self, cfg: dict, cmd: dict[str, Any]) -> dict:
200+ """Delete an export for an initiator."""
201+ self._asrt.assertDictEqual(
202+ cmd,
203+ {
204+ 'initiator': _ISCSI_IQN_OURS,
205+ 'portalGroup': _ISCSI_PORTAL_GROUP,
206+ 'volumeName': volumeName(fconst.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300207+ },
208+ )
Biser Milanov63fec2e2024-11-05 14:20:57 +0200209+ self._asrt.assertEqual(cfg['initiators']['1']['name'], cmd['initiator'])
210+ self._asrt.assertListEqual(
211+ cfg['initiators']['1']['exports'],
212+ [{'portalGroup': _ISCSI_PORTAL_GROUP,
213+ 'target': cfg['targets']['1']['name']}])
Peter Pentchevea354462023-07-18 11:15:56 +0300214+
Biser Milanov63fec2e2024-11-05 14:20:57 +0200215+ del cfg['initiators']['1']
216+ return cfg
217+
218+ def _handle_create_initiator(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300219+ """Add a whole new initiator."""
220+ self._asrt.assertDictEqual(
221+ cmd,
222+ {
223+ 'name': _ISCSI_IQN_OURS,
224+ 'username': '',
225+ 'secret': '',
226+ },
227+ )
228+ self._asrt.assertNotIn(
229+ cmd['name'],
Biser Milanov63fec2e2024-11-05 14:20:57 +0200230+ [init['name'] for init in cfg['initiators'].values()],
Peter Pentchevea354462023-07-18 11:15:56 +0300231+ )
Biser Milanov63fec2e2024-11-05 14:20:57 +0200232+ self._asrt.assertListEqual(sorted(cfg['initiators']), ['0'])
Peter Pentchevea354462023-07-18 11:15:56 +0300233+
Biser Milanov63fec2e2024-11-05 14:20:57 +0200234+ cfg['initiators'] = {
235+ **cfg['initiators'],
236+ '1': {'name': cmd['name'], 'exports': []},
237+ }
238+ return cfg
Peter Pentchevea354462023-07-18 11:15:56 +0300239+
Biser Milanov63fec2e2024-11-05 14:20:57 +0200240+
241+ def _handle_create_target(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300242+ """Add a target for a volume so that it may be exported."""
243+ self._asrt.assertDictEqual(
244+ cmd,
245+ {'volumeName': volumeName(fconst.VOLUME_ID)},
246+ )
Biser Milanov63fec2e2024-11-05 14:20:57 +0200247+ self._asrt.assertListEqual(sorted(cfg['targets']), ['0'])
248+ cfg['targets'] = {
249+ **cfg['targets'],
250+ '1': {
251+ 'name': targetName(fconst.VOLUME_ID),
252+ 'volume': volumeName(fconst.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300253+ },
Biser Milanov63fec2e2024-11-05 14:20:57 +0200254+ }
255+ return cfg
256+
257+ def _handle_delete_target(self, cfg: dict, cmd: dict[str, Any]) -> dict:
258+ """Remove a target for a volume."""
259+ self._asrt.assertDictEqual(
260+ cmd,
261+ {'volumeName': volumeName(fconst.VOLUME_ID)},
Peter Pentchevea354462023-07-18 11:15:56 +0300262+ )
263+
Biser Milanov63fec2e2024-11-05 14:20:57 +0200264+ self._asrt.assertListEqual(sorted(cfg['targets']), ['0', '1'])
265+ del cfg['targets']['1']
266+ return cfg
267+
Peter Pentchevea354462023-07-18 11:15:56 +0300268+ def _handle_initiator_add_network(
269+ self,
Biser Milanov63fec2e2024-11-05 14:20:57 +0200270+ cfg: dict,
Peter Pentchevea354462023-07-18 11:15:56 +0300271+ cmd: dict[str, Any],
Biser Milanov63fec2e2024-11-05 14:20:57 +0200272+ ) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300273+ """Add a network that an initiator is allowed to log in from."""
274+ self._asrt.assertDictEqual(
275+ cmd,
276+ {
277+ 'initiator': _ISCSI_IQN_OURS,
278+ 'net': '0.0.0.0/0',
279+ },
280+ )
Biser Milanov63fec2e2024-11-05 14:20:57 +0200281+ return cfg
Peter Pentchevea354462023-07-18 11:15:56 +0300282+
283+ _CMD_HANDLERS = {
284+ 'createInitiator': _handle_create_initiator,
285+ 'createTarget': _handle_create_target,
Biser Milanov63fec2e2024-11-05 14:20:57 +0200286+ 'deleteTarget': _handle_delete_target,
Peter Pentchevea354462023-07-18 11:15:56 +0300287+ 'export': _handle_export,
Biser Milanov63fec2e2024-11-05 14:20:57 +0200288+ 'exportDelete': _handle_delete_export,
Peter Pentchevea354462023-07-18 11:15:56 +0300289+ 'initiatorAddNetwork': _handle_initiator_add_network,
290+ }
291+
Biser Milanov63fec2e2024-11-05 14:20:57 +0200292+ def post_iscsi_config(
Peter Pentchevea354462023-07-18 11:15:56 +0300293+ self,
294+ commands: dict[str, list[dict[str, dict[str, Any]]]],
295+ ) -> None:
296+ """Apply the requested changes to the iSCSI configuration.
297+
298+ This method adds a new config object to the configs list,
299+ making a shallow copy of the last one and applying the changes
300+ specified in the list of commands.
301+ """
302+ self._asrt.assertListEqual(sorted(commands), ['commands'])
303+ self._asrt.assertGreater(len(commands['commands']), 0)
304+ for cmd in commands['commands']:
305+ keys = sorted(cmd.keys())
306+ cmd_name = keys[0]
307+ self._asrt.assertListEqual(keys, [cmd_name])
308+ handler = self._CMD_HANDLERS[cmd_name]
309+ new_cfg = handler(self, self._configs[-1], cmd[cmd_name])
310+ self._configs.append(new_cfg)
311+
312+
313+_ISCSI_TEST_CASES = [
314+ IscsiTestCase(None, None, False, 4),
315+ IscsiTestCase(_ISCSI_IQN_OURS, None, False, 2),
316+ IscsiTestCase(_ISCSI_IQN_OURS, fconst.VOLUME_ID, False, 1),
317+ IscsiTestCase(_ISCSI_IQN_OURS, fconst.VOLUME_ID, True, 0),
318+]
319+
Biser Milanov63fec2e2024-11-05 14:20:57 +0200320
Peter Pentchevea354462023-07-18 11:15:56 +0300321 def MockSPConfig(section = 's01'):
322 res = {}
Biser Milanov63fec2e2024-11-05 14:20:57 +0200323@@ -377,7 +626,15 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300324 self.cfg.volume_backend_name = 'storpool_test'
325 self.cfg.storpool_template = None
326 self.cfg.storpool_replication = 3
327+ self.cfg.iscsi_cinder_volume = False
328+ self.cfg.iscsi_export_to = ''
Peter Pentchevea354462023-07-18 11:15:56 +0300329+ self.cfg.iscsi_learn_initiator_iqns = True
330+ self.cfg.iscsi_portal_group = _ISCSI_PORTAL_GROUP
Peter Pentchev9c24be92022-09-26 22:35:24 +0300331+
Peter Pentchevea354462023-07-18 11:15:56 +0300332+ self._setup_test_driver()
333
Peter Pentchev9c24be92022-09-26 22:35:24 +0300334+ def _setup_test_driver(self):
335+ """Initialize a StorPool driver as per the current configuration."""
336 mock_exec = mock.Mock()
337 mock_exec.return_value = ('', '')
338
Biser Milanov63fec2e2024-11-05 14:20:57 +0200339@@ -394,7 +651,7 @@ class StorPoolTestCase(test.TestCase):
340 self.driver.check_for_setup_error()
Peter Pentchev9c24be92022-09-26 22:35:24 +0300341
342 @ddt.data(
343- (5, TypeError),
344+ (5, (TypeError, AttributeError)),
345 ({'no-host': None}, KeyError),
346 ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
347 ({'host': 's01'}, None),
Biser Milanov63fec2e2024-11-05 14:20:57 +0200348@@ -410,7 +667,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300349 conn)
350
351 @ddt.data(
352- (5, TypeError),
353+ (5, (TypeError, AttributeError)),
354 ({'no-host': None}, KeyError),
355 ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
356 )
Biser Milanov63fec2e2024-11-05 14:20:57 +0200357@@ -449,7 +706,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev5a9f8a62023-12-06 10:40:18 +0200358 self.assertEqual(21, pool['total_capacity_gb'])
359 self.assertEqual(5, int(pool['free_capacity_gb']))
360
361- self.assertTrue(pool['multiattach'])
362+ self.assertFalse(pool['multiattach'])
363 self.assertFalse(pool['QoS_support'])
364 self.assertFalse(pool['thick_provisioning_support'])
365 self.assertTrue(pool['thin_provisioning_support'])
Biser Milanov63fec2e2024-11-05 14:20:57 +0200366@@ -852,3 +1109,178 @@ class StorPoolTestCase(test.TestCase):
367 'No such volume',
368 self.driver.revert_to_snapshot, None,
369 {'id': vol_id}, {'id': snap_id})
Peter Pentchev9c24be92022-09-26 22:35:24 +0300370+
371+ @ddt.data(
372+ # The default values
Peter Pentchevea354462023-07-18 11:15:56 +0300373+ ('', False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300374+
375+ # Export to all
Peter Pentchevea354462023-07-18 11:15:56 +0300376+ ('*', True, constants.ISCSI, _ISCSI_IQN_OURS, True),
377+ ('*', True, constants.ISCSI, _ISCSI_IQN_OURS, True),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300378+
379+ # Only export to the controller
Peter Pentchevea354462023-07-18 11:15:56 +0300380+ ('', False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300381+
382+ # Some of the not-fully-supported pattern lists
Peter Pentchevea354462023-07-18 11:15:56 +0300383+ (_ISCSI_PAT_OTHER, False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
384+ (_ISCSI_PAT_OTHER, False, constants.STORPOOL, _ISCSI_IQN_OTHER, True),
385+ (_ISCSI_PAT_BOTH, False, constants.STORPOOL, _ISCSI_IQN_OURS, True),
386+ (_ISCSI_PAT_BOTH, False, constants.STORPOOL, _ISCSI_IQN_OTHER, True),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300387+ )
388+ @ddt.unpack
389+ def test_wants_iscsi(self, iscsi_export_to, use_iscsi, storage_protocol,
390+ hostname, expected):
391+ """Check the "should this export use iSCSI?" detection."""
392+ self.cfg.iscsi_export_to = iscsi_export_to
393+ self._setup_test_driver()
394+ self.assertEqual(self.driver._use_iscsi, use_iscsi)
395+
396+ # Make sure the driver reports the correct protocol in the stats
397+ self.driver._update_volume_stats()
398+ self.assertEqual(self.driver._stats["vendor_name"], "StorPool")
399+ self.assertEqual(self.driver._stats["storage_protocol"],
400+ storage_protocol)
401+
402+ def check(conn, forced, expected):
403+ """Pass partially or completely valid connector info."""
404+ for initiator in (None, hostname):
Peter Pentchevea354462023-07-18 11:15:56 +0300405+ for host in (None, _ISCSI_IQN_THIRD):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300406+ self.assertEqual(
407+ self.driver._connector_wants_iscsi({
408+ "host": host,
409+ "initiator": initiator,
410+ **conn,
411+ }),
412+ expected if initiator is not None and host is not None
413+ else forced)
414+
415+ # If iscsi_cinder_volume is set and this is the controller, then yes.
416+ check({"storpool_wants_iscsi": True}, True, True)
417+
418+ # If iscsi_cinder_volume is not set or this is not the controller, then
419+ # look at the specified expected value.
420+ check({"storpool_wants_iscsi": False}, use_iscsi, expected)
421+ check({}, use_iscsi, expected)
Peter Pentchevea354462023-07-18 11:15:56 +0300422+
423+ def _validate_iscsi_config(
424+ self,
Biser Milanov63fec2e2024-11-05 14:20:57 +0200425+ cfg: dict,
Peter Pentchevea354462023-07-18 11:15:56 +0300426+ res: dict[str, Any],
427+ tcase: IscsiTestCase,
428+ ) -> None:
429+ """Make sure the returned structure makes sense."""
430+ initiator = res['initiator']
Biser Milanov63fec2e2024-11-05 14:20:57 +0200431+ cfg_initiator = cfg['initiators'].get('1')
Peter Pentchevea354462023-07-18 11:15:56 +0300432+
Biser Milanov63fec2e2024-11-05 14:20:57 +0200433+ self.assertIs(res['cfg']['iscsi'], cfg)
434+ self.assertEqual(res['pg']['name'], _ISCSI_PORTAL_GROUP)
Peter Pentchevea354462023-07-18 11:15:56 +0300435+
436+ if tcase.initiator is None:
437+ self.assertIsNone(initiator)
438+ else:
439+ self.assertIsNotNone(initiator)
440+ self.assertEqual(initiator, cfg_initiator)
441+
442+ if tcase.volume is None:
443+ self.assertIsNone(res['target'])
444+ else:
445+ self.assertIsNotNone(res['target'])
Biser Milanov63fec2e2024-11-05 14:20:57 +0200446+ self.assertEqual(res['target'], cfg['targets'].get('1'))
Peter Pentchevea354462023-07-18 11:15:56 +0300447+
448+ if tcase.initiator is None:
449+ self.assertIsNone(cfg_initiator)
450+ self.assertIsNone(res['export'])
451+ else:
452+ self.assertIsNotNone(cfg_initiator)
453+ if tcase.exported:
454+ self.assertIsNotNone(res['export'])
Biser Milanov63fec2e2024-11-05 14:20:57 +0200455+ self.assertEqual(res['export'], cfg_initiator['exports'][0])
Peter Pentchevea354462023-07-18 11:15:56 +0300456+ else:
457+ self.assertIsNone(res['export'])
458+
459+ @ddt.data(*_ISCSI_TEST_CASES)
460+ def test_iscsi_get_config(self, tcase: IscsiTestCase) -> None:
461+ """Make sure the StorPool iSCSI configuration is parsed correctly."""
462+ cfg_orig = MockIscsiConfig.build(tcase)
463+ configs = [cfg_orig]
464+ iapi = MockIscsiAPI(configs, self)
Biser Milanov63fec2e2024-11-05 14:20:57 +0200465+ with mock.patch.object(self.driver, '_sp_api', iapi):
Peter Pentchevea354462023-07-18 11:15:56 +0300466+ res = self.driver._get_iscsi_config(
467+ _ISCSI_IQN_OURS,
468+ fconst.VOLUME_ID,
469+ )
470+
471+ self._validate_iscsi_config(cfg_orig, res, tcase)
472+
473+ @ddt.data(*_ISCSI_TEST_CASES)
474+ def test_iscsi_create_export(self, tcase: IscsiTestCase) -> None:
475+ """Make sure _create_iscsi_export() makes the right API calls."""
476+ cfg_orig = MockIscsiConfig.build(tcase)
477+ configs = [cfg_orig]
478+ iapi = MockIscsiAPI(configs, self)
Biser Milanov63fec2e2024-11-05 14:20:57 +0200479+ with mock.patch.object(self.driver, '_sp_api', iapi):
Peter Pentchevea354462023-07-18 11:15:56 +0300480+ self.driver._create_iscsi_export(
481+ {
482+ 'id': fconst.VOLUME_ID,
483+ 'display_name': fconst.VOLUME_NAME,
484+ },
485+ {
486+ # Yeah, okay, so we cheat a little bit here...
487+ 'host': _ISCSI_IQN_OURS + '.hostname',
488+ 'initiator': _ISCSI_IQN_OURS,
489+ },
490+ )
491+
492+ self.assertEqual(len(configs), tcase.commands_count + 1)
493+ cfg_final = configs[-1]
Biser Milanov63fec2e2024-11-05 14:20:57 +0200494+ self.assertEqual(cfg_final['initiators']['1']['name'], _ISCSI_IQN_OURS)
Peter Pentchevea354462023-07-18 11:15:56 +0300495+ self.assertEqual(
Biser Milanov63fec2e2024-11-05 14:20:57 +0200496+ cfg_final['initiators']['1']['exports'][0]['target'],
Peter Pentchevea354462023-07-18 11:15:56 +0300497+ targetName(fconst.VOLUME_ID),
498+ )
499+ self.assertEqual(
Biser Milanov63fec2e2024-11-05 14:20:57 +0200500+ cfg_final['targets']['1']['volume'],
Peter Pentchevea354462023-07-18 11:15:56 +0300501+ volumeName(fconst.VOLUME_ID),
502+ )
Biser Milanov63fec2e2024-11-05 14:20:57 +0200503+
504+ @ddt.data(*_ISCSI_TEST_CASES)
505+ def test_remove_iscsi_export(self, tcase: IscsiTestCase):
506+ cfg_orig = MockIscsiConfig.build(tcase)
507+ configs = [cfg_orig]
508+ iapi = MockIscsiAPI(configs, self)
509+
510+ def _target_exists(cfg: dict, volume: str) -> bool:
511+ for name, target in cfg['targets'].items():
512+ if target['volume'] == volumeName(volume):
513+ return True
514+ return False
515+
516+ def _export_exists(cfg: dict, volume: str) -> bool:
517+ for name, initiator in cfg['initiators'].items():
518+ for export in initiator['exports']:
519+ if export['target'] == targetName(volume):
520+ return True
521+ return False
522+
523+ if tcase.exported:
524+ self.assertTrue(
525+ _target_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
526+ self.assertTrue(
527+ _export_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
528+
529+ with mock.patch.object(self.driver, '_sp_api', iapi):
530+ self.driver._remove_iscsi_export(
531+ {
532+ 'id': fconst.VOLUME_ID,
533+ 'display_name': fconst.VOLUME_NAME,
534+ },
535+ {
536+ 'host': _ISCSI_IQN_OURS + '.hostname',
537+ 'initiator': _ISCSI_IQN_OURS,
538+ },
539+ )
540+
541+ self.assertFalse(
542+ _target_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
543+ self.assertFalse(
544+ _export_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Peter Pentchevacaaa382023-02-28 11:26:13 +0200545diff --git a/cinder/volume/drivers/storpool.py b/cinder/volume/drivers/storpool.py
Biser Milanov63fec2e2024-11-05 14:20:57 +0200546index dd57cf8c5..acdabe558 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +0300547--- a/cinder/volume/drivers/storpool.py
548+++ b/cinder/volume/drivers/storpool.py
Biser Milanov63fec2e2024-11-05 14:20:57 +0200549@@ -17,6 +17,7 @@
Peter Pentchev9c24be92022-09-26 22:35:24 +0300550
Biser Milanov63fec2e2024-11-05 14:20:57 +0200551 import configparser
552 import errno
Peter Pentchev9c24be92022-09-26 22:35:24 +0300553+import fnmatch
Biser Milanov63fec2e2024-11-05 14:20:57 +0200554 import http.client
555 import json
556 import os
557@@ -28,7 +29,10 @@ import time
Peter Pentchev9c24be92022-09-26 22:35:24 +0300558 from oslo_config import cfg
Biser Milanov63fec2e2024-11-05 14:20:57 +0200559 from oslo_log import log as logging
560 from oslo_utils import excutils
561+from oslo_utils import netutils
562 from oslo_utils import units
563+from oslo_utils import uuidutils
564+import six
565
566 from cinder.common import constants
567 from cinder import context
568@@ -46,6 +50,31 @@ DEV_STORPOOL_BYID = pathlib.Path('/dev/storpool-byid')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300569
570
571 storpool_opts = [
572+ cfg.BoolOpt('iscsi_cinder_volume',
573+ default=False,
574+ help='Let the cinder-volume service use iSCSI instead of '
575+ 'the StorPool block device driver for accessing '
576+ 'StorPool volumes, e.g. when creating a volume from '
577+ 'an image or vice versa.'),
578+ cfg.StrOpt('iscsi_export_to',
579+ default='',
580+ help='Whether to export volumes using iSCSI. '
581+ 'An empty string (the default) makes the driver export '
582+ 'all volumes using the StorPool native network protocol. '
583+ 'The value "*" makes the driver export all volumes using '
584+ 'iSCSI. '
585+ 'Any other value leads to an experimental not fully '
586+ 'supported configuration and is interpreted as '
587+ 'a whitespace-separated list of patterns for IQNs for '
588+ 'hosts that need volumes to be exported via iSCSI, e.g. '
589+ '"iqn.1991-05.com.microsoft:\\*" for Windows hosts.'),
590+ cfg.BoolOpt('iscsi_learn_initiator_iqns',
591+ default=True,
592+ help='Create a StorPool record for a new initiator as soon as '
593+ 'Cinder asks for a volume to be exported to it.'),
594+ cfg.StrOpt('iscsi_portal_group',
595+ default=None,
596+ help='The portal group to export volumes via iSCSI in.'),
597 cfg.StrOpt('storpool_template',
598 default=None,
599 help='The StorPool template for volumes with no type.'),
Biser Milanov63fec2e2024-11-05 14:20:57 +0200600@@ -61,6 +90,28 @@ CONF = cfg.CONF
601 CONF.register_opts(storpool_opts, group=configuration.SHARED_CONF_GROUP)
602
603
604+def _extract_cinder_ids(urls):
605+ ids = []
606+ for url in urls:
607+ # The url can also be None and a TypeError is raised
608+ # TypeError: a bytes-like object is required, not 'str'
609+ if not url:
610+ continue
611+ parts = netutils.urlsplit(url)
612+ if parts.scheme == 'cinder':
613+ if parts.path:
614+ vol_id = parts.path.split('/')[-1]
615+ else:
616+ vol_id = parts.netloc
617+ if uuidutils.is_uuid_like(vol_id):
618+ ids.append(vol_id)
619+ else:
620+ LOG.debug("Ignoring malformed image location uri "
621+ "'%(url)s'", {'url': url})
622+
623+ return ids
624+
625+
626 class StorPoolConfigurationInvalid(exception.CinderException):
627 message = _("Invalid parameter %(param)s in the %(section)s section "
628 "of the /etc/storpool.conf file: %(error)s")
629@@ -233,6 +284,12 @@ class StorPoolAPI:
630 self._api_call(
631 'POST', f'/ctrl/1.0/MultiCluster/SnapshotDelete/{snapshot}')
632
633+ def get_iscsi_config(self):
634+ return self._api_call('GET', '/ctrl/1.0/iSCSIConfig')
635+
636+ def post_iscsi_config(self, data):
637+ return self._api_call('POST', '/ctrl/1.0/iSCSIConfig', data)
638+
639
640 @interface.volumedriver
641 class StorPoolDriver(driver.VolumeDriver):
642@@ -264,9 +321,10 @@ class StorPoolDriver(driver.VolumeDriver):
643 add ignore_errors to the internal _detach_volume() method
644 1.2.3 - Advertise some more driver capabilities.
645 2.0.0 - Implement revert_to_snapshot().
646+ 2.0.1 - Add iSCSI export support.
647 """
648
649- VERSION = '2.0.0'
650+ VERSION = '2.0.1'
651 CI_WIKI_NAME = 'StorPool_distributed_storage_CI'
652
653 def __init__(self, *args, **kwargs):
654@@ -277,6 +335,7 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300655 self._ourIdInt = None
Biser Milanov63fec2e2024-11-05 14:20:57 +0200656 self._sp_api = None
657 self._volume_prefix = None
Peter Pentchev9c24be92022-09-26 22:35:24 +0300658+ self._use_iscsi = None
659
660 @staticmethod
661 def get_driver_options():
Biser Milanov63fec2e2024-11-05 14:20:57 +0200662@@ -351,10 +410,326 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300663 raise StorPoolConfigurationInvalid(
664 section=hostname, param='SP_OURID', error=e)
665
666+ def _connector_wants_iscsi(self, connector):
667+ """Should we do this export via iSCSI?
668+
669+ Check the configuration to determine whether this connector is
670+ expected to provide iSCSI exports as opposed to native StorPool
671+ protocol ones. Match the initiator's IQN against the list of
672+ patterns supplied in the "iscsi_export_to" configuration setting.
673+ """
674+ if connector is None:
675+ return False
676+ if self._use_iscsi:
677+ LOG.debug(' - forcing iSCSI for all exported volumes')
678+ return True
679+ if connector.get('storpool_wants_iscsi'):
680+ LOG.debug(' - forcing iSCSI for the controller')
681+ return True
682+
683+ try:
684+ iqn = connector.get('initiator')
685+ except Exception:
686+ iqn = None
687+ try:
688+ host = connector.get('host')
689+ except Exception:
690+ host = None
691+ if iqn is None or host is None:
692+ LOG.debug(' - this connector certainly does not want iSCSI')
693+ return False
694+
695+ LOG.debug(' - check whether %(host)s (%(iqn)s) wants iSCSI',
696+ {
697+ 'host': host,
698+ 'iqn': iqn,
699+ })
700+
701+ export_to = self.configuration.iscsi_export_to
702+ if export_to is None:
703+ return False
704+
705+ for pat in export_to.split():
706+ LOG.debug(' - matching against %(pat)s', {'pat': pat})
707+ if fnmatch.fnmatch(iqn, pat):
708+ LOG.debug(' - got it!')
709+ return True
710+ LOG.debug(' - nope')
711+ return False
712+
713 def validate_connector(self, connector):
714+ if self._connector_wants_iscsi(connector):
715+ return True
716 return self._storpool_client_id(connector) >= 0
717
718+ def _get_iscsi_config(self, iqn, volume_id):
719+ """Get the StorPool iSCSI config items pertaining to this volume.
720+
721+ Find the elements of the StorPool iSCSI configuration tree that
722+ will be needed to create, ensure, or remove the iSCSI export of
723+ the specified volume to the specified initiator.
724+ """
Biser Milanov63fec2e2024-11-05 14:20:57 +0200725+ cfg = self._sp_api.get_iscsi_config()
Peter Pentchev9c24be92022-09-26 22:35:24 +0300726+
727+ pg_name = self.configuration.iscsi_portal_group
728+ pg_found = [
Biser Milanov63fec2e2024-11-05 14:20:57 +0200729+ pg for pg in cfg['iscsi']['portalGroups'].values() if pg['name'] == pg_name
Peter Pentchev9c24be92022-09-26 22:35:24 +0300730+ ]
731+ if not pg_found:
732+ raise Exception('StorPool Cinder iSCSI configuration error: '
733+ 'no portal group "{pg}"'.format(pg=pg_name))
734+ pg = pg_found[0]
735+
736+ # Do we know about this initiator?
737+ i_found = [
Biser Milanov63fec2e2024-11-05 14:20:57 +0200738+ init for init in cfg['iscsi']['initiators'].values() if init['name'] == iqn
Peter Pentchev9c24be92022-09-26 22:35:24 +0300739+ ]
740+ if i_found:
741+ initiator = i_found[0]
742+ else:
743+ initiator = None
744+
745+ # Is this volume already being exported?
Biser Milanov63fec2e2024-11-05 14:20:57 +0200746+ volname = self._os_to_sp_volume_name(volume_id)
Peter Pentchev9c24be92022-09-26 22:35:24 +0300747+ t_found = [
Biser Milanov63fec2e2024-11-05 14:20:57 +0200748+ tgt for tgt in cfg['iscsi']['targets'].values() if tgt['volume'] == volname
Peter Pentchev9c24be92022-09-26 22:35:24 +0300749+ ]
750+ if t_found:
751+ target = t_found[0]
752+ else:
753+ target = None
754+
755+ # OK, so is this volume being exported to this initiator?
756+ export = None
757+ if initiator is not None and target is not None:
758+ e_found = [
Biser Milanov63fec2e2024-11-05 14:20:57 +0200759+ exp for exp in initiator['exports']
760+ if exp['portalGroup'] == pg['name'] and exp['target'] == target['name']
Peter Pentchev9c24be92022-09-26 22:35:24 +0300761+ ]
762+ if e_found:
763+ export = e_found[0]
764+
765+ return {
766+ 'cfg': cfg,
767+ 'pg': pg,
768+ 'initiator': initiator,
769+ 'target': target,
770+ 'export': export,
771+ 'volume_name': volname,
772+ 'volume_id': volume_id,
773+ }
774+
775+ def _create_iscsi_export(self, volume, connector):
776+ """Create (if needed) an iSCSI export for the StorPool volume."""
777+ LOG.debug(
778+ '_create_iscsi_export() invoked for volume '
779+ '"%(vol_name)s" (%(vol_id)s) connector %(connector)s',
780+ {
781+ 'vol_name': volume['display_name'],
782+ 'vol_id': volume['id'],
783+ 'connector': connector,
784+ }
785+ )
786+ iqn = connector['initiator']
787+ try:
788+ cfg = self._get_iscsi_config(iqn, volume['id'])
789+ except Exception as exc:
790+ LOG.error(
791+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
792+ )
793+ raise
794+
795+ if cfg['initiator'] is None:
796+ if not (self.configuration.iscsi_learn_initiator_iqns or
797+ self.configuration.iscsi_cinder_volume and
798+ connector.get('storpool_wants_iscsi')):
799+ raise Exception('The "{iqn}" initiator IQN for the "{host}" '
800+ 'host is not defined in the StorPool '
801+ 'configuration.'
802+ .format(iqn=iqn, host=connector['host']))
803+ else:
804+ LOG.info('Creating a StorPool iSCSI initiator '
805+ 'for "{host}s" ({iqn}s)',
806+ {'host': connector['host'], 'iqn': iqn})
Biser Milanov63fec2e2024-11-05 14:20:57 +0200807+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300808+ 'commands': [
809+ {
810+ 'createInitiator': {
811+ 'name': iqn,
812+ 'username': '',
813+ 'secret': '',
814+ },
815+ },
816+ {
817+ 'initiatorAddNetwork': {
818+ 'initiator': iqn,
819+ 'net': '0.0.0.0/0',
820+ },
821+ },
822+ ]
823+ })
824+
825+ if cfg['target'] is None:
826+ LOG.info(
827+ 'Creating a StorPool iSCSI target '
828+ 'for the "%(vol_name)s" volume (%(vol_id)s)',
829+ {
830+ 'vol_name': volume['display_name'],
831+ 'vol_id': volume['id'],
832+ }
833+ )
Biser Milanov63fec2e2024-11-05 14:20:57 +0200834+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300835+ 'commands': [
836+ {
837+ 'createTarget': {
838+ 'volumeName': cfg['volume_name'],
839+ },
840+ },
841+ ]
842+ })
843+ cfg = self._get_iscsi_config(iqn, volume['id'])
844+
845+ if cfg['export'] is None:
846+ LOG.info('Creating a StorPool iSCSI export '
847+ 'for the "{vol_name}s" volume ({vol_id}s) '
848+ 'to the "{host}s" initiator ({iqn}s) '
849+ 'in the "{pg}s" portal group',
850+ {
851+ 'vol_name': volume['display_name'],
852+ 'vol_id': volume['id'],
853+ 'host': connector['host'],
854+ 'iqn': iqn,
Biser Milanov63fec2e2024-11-05 14:20:57 +0200855+ 'pg': cfg['pg']['name']
Peter Pentchev9c24be92022-09-26 22:35:24 +0300856+ })
Biser Milanov63fec2e2024-11-05 14:20:57 +0200857+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300858+ 'commands': [
859+ {
860+ 'export': {
861+ 'initiator': iqn,
Biser Milanov63fec2e2024-11-05 14:20:57 +0200862+ 'portalGroup': cfg['pg']['name'],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300863+ 'volumeName': cfg['volume_name'],
864+ },
865+ },
866+ ]
867+ })
868+
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200869+ target_portals = [
Biser Milanov63fec2e2024-11-05 14:20:57 +0200870+ "{addr}:3260".format(addr=net['address'])
871+ for net in cfg['pg']['networks']
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200872+ ]
Biser Milanov63fec2e2024-11-05 14:20:57 +0200873+ target_iqns = [cfg['target']['name']] * len(target_portals)
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200874+ target_luns = [0] * len(target_portals)
875+ if connector.get('multipath', False):
876+ multipath_settings = {
877+ 'target_iqns': target_iqns,
878+ 'target_portals': target_portals,
879+ 'target_luns': target_luns,
880+ }
881+ else:
882+ multipath_settings = {}
883+
Peter Pentchev9c24be92022-09-26 22:35:24 +0300884+ res = {
885+ 'driver_volume_type': 'iscsi',
886+ 'data': {
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200887+ **multipath_settings,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300888+ 'target_discovered': False,
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200889+ 'target_iqn': target_iqns[0],
890+ 'target_portal': target_portals[0],
891+ 'target_lun': target_luns[0],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300892+ 'volume_id': volume['id'],
893+ 'discard': True,
894+ },
895+ }
896+ LOG.debug('returning %(res)s', {'res': res})
897+ return res
898+
899+ def _remove_iscsi_export(self, volume, connector):
900+ """Remove an iSCSI export for the specified StorPool volume."""
901+ LOG.debug(
902+ '_remove_iscsi_export() invoked for volume '
903+ '"%(vol_name)s" (%(vol_id)s) connector %(conn)s',
904+ {
905+ 'vol_name': volume['display_name'],
906+ 'vol_id': volume['id'],
907+ 'conn': connector,
908+ }
909+ )
910+ try:
911+ cfg = self._get_iscsi_config(connector['initiator'], volume['id'])
912+ except Exception as exc:
913+ LOG.error(
914+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
915+ )
916+ raise
917+
918+ if cfg['export'] is not None:
919+ LOG.info('Removing the StorPool iSCSI export '
920+ 'for the "%(vol_name)s" volume (%(vol_id)s) '
921+ 'to the "%(host)s" initiator (%(iqn)s) '
922+ 'in the "%(pg)s" portal group',
923+ {
924+ 'vol_name': volume['display_name'],
925+ 'vol_id': volume['id'],
926+ 'host': connector['host'],
927+ 'iqn': connector['initiator'],
Biser Milanov63fec2e2024-11-05 14:20:57 +0200928+ 'pg': cfg['pg']['name'],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300929+ })
930+ try:
Biser Milanov63fec2e2024-11-05 14:20:57 +0200931+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300932+ 'commands': [
933+ {
934+ 'exportDelete': {
Biser Milanov63fec2e2024-11-05 14:20:57 +0200935+ 'initiator': cfg['initiator']['name'],
936+ 'portalGroup': cfg['pg']['name'],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300937+ 'volumeName': cfg['volume_name'],
938+ },
939+ },
940+ ]
941+ })
Biser Milanov63fec2e2024-11-05 14:20:57 +0200942+ except StorPoolAPIError as e:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300943+ if e.name not in ('objectExists', 'objectDoesNotExist'):
944+ raise
945+ LOG.info('Looks like somebody beat us to it')
946+
947+ if cfg['target'] is not None:
948+ last = True
Biser Milanov63fec2e2024-11-05 14:20:57 +0200949+ for initiator in cfg['cfg']['iscsi']['initiators'].values():
950+ if initiator['name'] == cfg['initiator']['name']:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300951+ continue
Biser Milanov63fec2e2024-11-05 14:20:57 +0200952+ for exp in initiator['exports']:
953+ if exp['target'] == cfg['target']['name']:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300954+ last = False
955+ break
956+ if not last:
957+ break
958+
959+ if last:
960+ LOG.info(
961+ 'Removing the StorPool iSCSI target '
962+ 'for the "{vol_name}s" volume ({vol_id}s)',
963+ {
964+ 'vol_name': volume['display_name'],
965+ 'vol_id': volume['id'],
966+ }
967+ )
968+ try:
Biser Milanov63fec2e2024-11-05 14:20:57 +0200969+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300970+ 'commands': [
971+ {
972+ 'deleteTarget': {
973+ 'volumeName': cfg['volume_name'],
974+ },
975+ },
976+ ]
977+ })
Biser Milanov63fec2e2024-11-05 14:20:57 +0200978+ except StorPoolAPIError as e:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300979+ if e.name not in ('objectDoesNotExist', 'invalidParam'):
980+ raise
981+ LOG.info('Looks like somebody beat us to it')
982+
983 def initialize_connection(self, volume, connector):
984+ if self._connector_wants_iscsi(connector):
985+ return self._create_iscsi_export(volume, connector)
986 return {'driver_volume_type': 'storpool',
987 'data': {
988 'client_id': self._storpool_client_id(connector),
Biser Milanov63fec2e2024-11-05 14:20:57 +0200989@@ -363,6 +738,9 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300990 }}
991
992 def terminate_connection(self, volume, connector, **kwargs):
993+ if self._connector_wants_iscsi(connector):
994+ LOG.debug('- removing an iSCSI export')
995+ self._remove_iscsi_export(volume, connector)
996 pass
997
998 def create_snapshot(self, snapshot):
Biser Milanov63fec2e2024-11-05 14:20:57 +0200999@@ -464,11 +842,20 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001000 )
1001
1002 def create_export(self, context, volume, connector):
1003- pass
1004+ if self._connector_wants_iscsi(connector):
1005+ LOG.debug('- creating an iSCSI export')
1006+ self._create_iscsi_export(volume, connector)
1007
1008 def remove_export(self, context, volume):
1009 pass
1010
1011+ def _attach_volume(self, context, volume, properties, remote=False):
1012+ if self.configuration.iscsi_cinder_volume and not remote:
1013+ LOG.debug('- adding the "storpool_wants_iscsi" flag')
1014+ properties['storpool_wants_iscsi'] = True
1015+
1016+ return super()._attach_volume(context, volume, properties, remote)
1017+
1018 def delete_volume(self, volume):
Biser Milanov63fec2e2024-11-05 14:20:57 +02001019 name = self._os_to_sp_volume_name(volume['id'])
Peter Pentchev9c24be92022-09-26 22:35:24 +03001020 try:
Biser Milanov63fec2e2024-11-05 14:20:57 +02001021@@ -502,6 +889,17 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001022 LOG.error("StorPoolDriver API initialization failed: %s", e)
1023 raise
1024
1025+ export_to = self.configuration.iscsi_export_to
1026+ export_to_set = export_to is not None and export_to.split()
1027+ vol_iscsi = self.configuration.iscsi_cinder_volume
1028+ pg_name = self.configuration.iscsi_portal_group
1029+ if (export_to_set or vol_iscsi) and pg_name is None:
1030+ msg = _('The "iscsi_portal_group" option is required if '
1031+ 'any patterns are listed in "iscsi_export_to"')
1032+ raise exception.VolumeDriverException(message=msg)
1033+
1034+ self._use_iscsi = export_to == "*"
1035+
1036 def _update_volume_stats(self):
1037 try:
Biser Milanov63fec2e2024-11-05 14:20:57 +02001038 dl = self._sp_api.disks_list()
1039@@ -527,7 +925,7 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001040 'total_capacity_gb': total / units.Gi,
1041 'free_capacity_gb': free / units.Gi,
1042 'reserved_percentage': 0,
1043- 'multiattach': True,
Peter Pentchev5a9f8a62023-12-06 10:40:18 +02001044+ 'multiattach': self._use_iscsi,
Peter Pentchev9c24be92022-09-26 22:35:24 +03001045 'QoS_support': False,
1046 'thick_provisioning_support': False,
1047 'thin_provisioning_support': True,
Biser Milanov63fec2e2024-11-05 14:20:57 +02001048@@ -546,7 +944,9 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001049 'volume_backend_name') or 'storpool',
1050 'vendor_name': 'StorPool',
1051 'driver_version': self.VERSION,
1052- 'storage_protocol': constants.STORPOOL,
1053+ 'storage_protocol': (
1054+ constants.ISCSI if self._use_iscsi else constants.STORPOOL
1055+ ),
Peter Pentchevacaaa382023-02-28 11:26:13 +02001056 # Driver capabilities
Peter Pentchev9c24be92022-09-26 22:35:24 +03001057 'clone_across_pools': True,
1058 'sparse_copy_volume': True,
Peter Pentchevacaaa382023-02-28 11:26:13 +02001059diff --git a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
Biser Milanov63fec2e2024-11-05 14:20:57 +02001060index d2c5895a9..1ba0d2862 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +03001061--- a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
1062+++ b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
Peter Pentchevacaaa382023-02-28 11:26:13 +02001063@@ -19,12 +19,15 @@ Prerequisites
Peter Pentchev9c24be92022-09-26 22:35:24 +03001064 * The controller and all the compute nodes must have access to the StorPool
1065 API service.
1066
1067-* All nodes where StorPool-backed volumes will be attached must have access to
1068+* If iSCSI is not being used as a transport (see below), all nodes where
1069+ StorPool-backed volumes will be attached must have access to
1070 the StorPool data network and run the ``storpool_block`` service.
1071
1072-* If StorPool-backed Cinder volumes need to be created directly from Glance
1073- images, then the node running the ``cinder-volume`` service must also have
1074- access to the StorPool data network and run the ``storpool_block`` service.
1075+* If Glance uses Cinder as its image store, or if StorPool-backed Cinder
1076+ volumes need to be created directly from Glance images, and iSCSI is not
1077+ being used as a transport, then the node running the ``cinder-volume``
1078+ service must also have access to the StorPool data network and run
1079+ the ``storpool_block`` service.
1080
1081 * All nodes that need to access the StorPool API (the compute nodes and
1082 the node running the ``cinder-volume`` service) must have the following
Biser Milanov63fec2e2024-11-05 14:20:57 +02001083@@ -34,6 +37,33 @@ Prerequisites
Peter Pentchev9c24be92022-09-26 22:35:24 +03001084 * the storpool Python bindings package
1085 * the storpool.spopenstack Python helper package
1086
1087+Using iSCSI as the transport protocol
1088+-------------------------------------
1089+
1090+The StorPool distributed storage system uses its own, highly optimized and
1091+tailored for its specifics, network protocol for communication between
1092+the storage servers and the clients (the OpenStack cluster nodes where
1093+StorPool-backed volumes will be attached). There are cases when granting
1094+various nodes access to the StorPool data network or installing and
1095+running the ``storpool_block`` client service on them may pose difficulties.
1096+The StorPool servers may also expose the user-created volumes and snapshots
1097+using the standard iSCSI protocol that only requires TCP routing and
1098+connectivity between the storage servers and the StorPool clients.
1099+The StorPool Cinder driver may be configured to export volumes and
1100+snapshots via iSCSI using the ``iscsi_export_to`` and ``iscsi_portal_group``
1101+configuration options.
1102+
1103+Additionally, even if e.g. the hypervisor nodes running Nova will use
1104+the StorPool network protocol and run the ``storpool_block`` service
1105+(so the ``iscsi_export_to`` option has its default empty string value),
1106+the ``iscsi_cinder_volume`` option configures the StorPool Cinder driver
1107+so that only the ``cinder-volume`` service will use the iSCSI protocol when
1108+attaching volumes and snapshots to transfer data to and from Glance images.
1109+
Biser Milanov63fec2e2024-11-05 14:20:57 +02001110+Multiattach support for StorPool is only enabled if iSCSI is used:
1111+``iscsi_export_to`` is set to ``*``, that is, when all StorPool volumes
1112+will be exported via iSCSI to all initiators.
1113+
Peter Pentchev9c24be92022-09-26 22:35:24 +03001114 Configuring the StorPool volume driver
1115 --------------------------------------
1116
Biser Milanov63fec2e2024-11-05 14:20:57 +02001117@@ -55,6 +85,32 @@ volume backend definition) and per volume type:
Peter Pentchev9c24be92022-09-26 22:35:24 +03001118 with the default placement constraints for the StorPool cluster.
1119 The default value for the chain replication is 3.
1120
Peter Pentchevea354462023-07-18 11:15:56 +03001121+In addition, if the iSCSI protocol is used to access the StorPool cluster as
1122+described in the previous section, the following options may be defined in
1123+the ``cinder.conf`` volume backend definition:
1124+
Peter Pentchev9c24be92022-09-26 22:35:24 +03001125+- ``iscsi_export_to``: if set to the value ``*``, the StorPool Cinder driver
1126+ will export volumes and snapshots using the iSCSI protocol instead of
1127+ the StorPool network protocol. The ``iscsi_portal_group`` option must also
1128+ be specified.
1129+
1130+- ``iscsi_portal_group``: if the ``iscsi_export_to`` option is set to
1131+ the value ``*`` or the ``iscsi_cinder_volume`` option is turned on,
1132+ this option specifies the name of the iSCSI portal group that Cinder
1133+ volumes will be exported to.
1134+
1135+- ``iscsi_cinder_volume``: if enabled, even if the ``iscsi_export_to`` option
1136+ has its default empty value, the ``cinder-volume`` service will use iSCSI
1137+ to attach the volumes and snapshots for transferring data to and from
Biser Milanov63fec2e2024-11-05 14:20:57 +02001138+ Glance images if Glance is configured to use the Cinder glance_store.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001139+
Peter Pentchevea354462023-07-18 11:15:56 +03001140+- ``iscsi_learn_initiator_iqns``: if enabled, the StorPool Cinder driver will
1141+ automatically use the StorPool API to create definitions for new initiators
1142+ in the StorPool cluster's configuration. This is the default behavior of
1143+ the driver; it may be disabled in the rare case if, e.g. because of site
1144+ policy, OpenStack iSCSI initiators (e.g. Nova hypervisors) need to be
1145+ explicitly allowed to use the StorPool iSCSI targets.
1146+
Peter Pentchev9c24be92022-09-26 22:35:24 +03001147 Using the StorPool volume driver
1148 --------------------------------
1149
Peter Pentchevacaaa382023-02-28 11:26:13 +02001150--
Biser Milanov63fec2e2024-11-05 14:20:57 +020011512.43.0
Peter Pentchevacaaa382023-02-28 11:26:13 +02001152