blob: b5f1ecc80bc8ea873d519147ed8a6c331ecdccc1 [file] [log] [blame]
Biser Milanovec3bf982024-11-27 17:42:17 +02001From 2acfdddf0794754aa0e32a56800e77387f75ce38 Mon Sep 17 00:00:00 2001
Peter Pentchev9c24be92022-09-26 22:35:24 +03002From: Peter Penchev <openstack-dev@storpool.com>
Biser Milanov8323a8c2024-11-20 10:53:27 +02003Date: Mon, 12 Mar 2018 12:00:10 +0200
Biser Milanovd684c1c2024-11-22 12:12:59 +02004Subject: [PATCH] Add iSCSI export support to the StorPool driver
Peter Pentchev9c24be92022-09-26 22:35:24 +03005
6Add four new driver options:
Biser Milanovd684c1c2024-11-22 12:12:59 +02007- storpool_iscsi_cinder_volume: use StorPool iSCSI attachments whenever
Peter Pentchev9c24be92022-09-26 22:35:24 +03008 the cinder-volume service needs to attach a volume to the controller,
9 e.g. for copying an image to a volume or vice versa
Biser Milanovd684c1c2024-11-22 12:12:59 +020010- storpool_iscsi_export_to:
11 - an empty string to use the StorPool native protocol for exporting
12 volumes
Peter Pentchev9c24be92022-09-26 22:35:24 +030013 - the string "*" to always use iSCSI for exporting volumes
14 - an experimental, not fully supported list of IQN patterns to export
15 volumes to using iSCSI; this results in a Cinder driver that exports
16 different volumes using different storage protocols
Biser Milanovd684c1c2024-11-22 12:12:59 +020017- storpool_iscsi_portal_group: the name of the iSCSI portal group
18 defined in the StorPool configuration to use for these export
19- storpool_iscsi_learn_initiator_iqns: automatically create StorPool
20 configuration records for an initiator when a volume is first exported
21 to it
Peter Pentchev9c24be92022-09-26 22:35:24 +030022
Biser Milanovd684c1c2024-11-22 12:12:59 +020023When exporting volumes via iSCSI, report the storage protocol as
24"iSCSI".
Peter Pentchev9c24be92022-09-26 22:35:24 +030025
26Change-Id: I9de64306e0e6976268df782053b0651dd1cca96f
27---
Biser Milanovda1b0682024-11-29 09:37:10 +020028 .../unit/volume/drivers/test_storpool.py | 521 +++++++++++++++++-
29 cinder/volume/drivers/storpool.py | 379 ++++++++++++-
30 .../drivers/storpool-volume-driver.rst | 68 ++-
31 .../storpool-iscsi-cefcfe590a07c5c7.yaml | 15 +
32 4 files changed, 972 insertions(+), 11 deletions(-)
33 create mode 100644 releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
Peter Pentchevacaaa382023-02-28 11:26:13 +020034diff --git a/cinder/tests/unit/volume/drivers/test_storpool.py b/cinder/tests/unit/volume/drivers/test_storpool.py
Biser Milanovbf1b0ea2025-01-20 16:36:46 +020035index 2015c734d..5a45775ce 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +030036--- a/cinder/tests/unit/volume/drivers/test_storpool.py
37+++ b/cinder/tests/unit/volume/drivers/test_storpool.py
Biser Milanovbf1b0ea2025-01-20 16:36:46 +020038@@ -13,9 +13,13 @@
39 # License for the specific language governing permissions and limitations
Biser Milanovda1b0682024-11-29 09:37:10 +020040 # under the License.
Peter Pentchevea354462023-07-18 11:15:56 +030041
Biser Milanovda1b0682024-11-29 09:37:10 +020042+from __future__ import annotations
Biser Milanovbf1b0ea2025-01-20 16:36:46 +020043
Peter Pentchevea354462023-07-18 11:15:56 +030044+import dataclasses
45 import itertools
46 import re
Biser Milanovbf1b0ea2025-01-20 16:36:46 +020047+import sys
Peter Pentchevea354462023-07-18 11:15:56 +030048+from typing import Any, NamedTuple, TYPE_CHECKING # noqa: H301
49 from unittest import mock
50
51 import ddt
Biser Milanovbf1b0ea2025-01-20 16:36:46 +020052@@ -23,13 +27,20 @@ from os_brick.initiator import storpool_utils
53 from os_brick.tests.initiator import test_storpool_utils
Peter Pentchevea354462023-07-18 11:15:56 +030054 from oslo_utils import units
Peter Pentchevea354462023-07-18 11:15:56 +030055
Biser Milanovda1b0682024-11-29 09:37:10 +020056+if TYPE_CHECKING:
57+ if sys.version_info >= (3, 11):
58+ from typing import Self
59+ else:
60+ from typing_extensions import Self
61+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +020062+
Peter Pentchev9c24be92022-09-26 22:35:24 +030063+from cinder.common import constants
64 from cinder import exception
Biser Milanovec3bf982024-11-27 17:42:17 +020065 from cinder.tests.unit import fake_constants
Peter Pentchev9c24be92022-09-26 22:35:24 +030066 from cinder.tests.unit import test
Biser Milanovbf1b0ea2025-01-20 16:36:46 +020067 from cinder.volume import configuration as conf
Biser Milanovda1b0682024-11-29 09:37:10 +020068 from cinder.volume.drivers import storpool as driver
69
Biser Milanovbf1b0ea2025-01-20 16:36:46 +020070-
71 volume_types = {
72 fake_constants.VOLUME_TYPE_ID: {},
73 fake_constants.VOLUME_TYPE2_ID: {'storpool_template': 'ssd'},
74@@ -58,6 +69,12 @@ def mock_volume_types(f):
75 def volumeName(vid):
76 return 'os--volume-{id}'.format(id=vid)
Peter Pentchevea354462023-07-18 11:15:56 +030077
78+_ISCSI_IQN_OURS = 'beleriand'
79+_ISCSI_IQN_OTHER = 'rohan'
80+_ISCSI_IQN_THIRD = 'gondor'
81+_ISCSI_PAT_OTHER = 'roh*'
82+_ISCSI_PAT_BOTH = '*riand roh*'
83+_ISCSI_PORTAL_GROUP = 'openstack_pg'
Biser Milanovbf1b0ea2025-01-20 16:36:46 +020084
85 def snapshotName(vtype, vid, more=None):
86 return 'os--{t}--{m}--snapshot-{id}'.format(
87@@ -67,6 +84,10 @@ def snapshotName(vtype, vid, more=None):
88 )
Peter Pentchevea354462023-07-18 11:15:56 +030089
90
91+def targetName(vid):
92+ return 'iqn.2012-11.storpool:{id}'.format(id=vid)
93+
94+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +020095 class MockAPI(object):
96 def __init__(self, *args):
97 self._disks = {}
98@@ -178,6 +199,241 @@ class MockVolumeDB(object):
99 'volume_type': self.vol_types.get(vid),
100 }
Peter Pentchevea354462023-07-18 11:15:56 +0300101
Peter Pentchevea354462023-07-18 11:15:56 +0300102+class IscsiTestCase(NamedTuple):
103+ """A single test case for the iSCSI config and export methods."""
104+
105+ initiator: str | None
106+ volume: str | None
107+ exported: bool
108+ commands_count: int
109+
110+
111+@dataclasses.dataclass(frozen=True)
112+class MockIscsiConfig:
113+ """Mock the structure returned by the "get current config" query."""
114+
Peter Pentchevea354462023-07-18 11:15:56 +0300115+ @classmethod
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200116+ def build(cls, tcase: IscsiTestCase) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300117+ """Build a test config structure."""
118+ initiators = {
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200119+ '0': {'name': _ISCSI_IQN_OTHER, 'exports': []},
Peter Pentchevea354462023-07-18 11:15:56 +0300120+ }
121+ if tcase.initiator is not None:
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200122+ initiators['1'] = {
123+ 'name': tcase.initiator,
124+ 'exports': (
Peter Pentchevea354462023-07-18 11:15:56 +0300125+ [
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200126+ {
127+ 'portalGroup': _ISCSI_PORTAL_GROUP,
128+ 'target': targetName(tcase.volume),
129+ },
Peter Pentchevea354462023-07-18 11:15:56 +0300130+ ]
131+ if tcase.exported
132+ else []
133+ ),
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200134+ }
Peter Pentchevea354462023-07-18 11:15:56 +0300135+
136+ targets = {
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200137+ '0': {
138+ 'name': targetName(fake_constants.VOLUME2_ID),
139+ 'volume': volumeName(fake_constants.VOLUME2_ID),
140+ },
Peter Pentchevea354462023-07-18 11:15:56 +0300141+ }
142+ if tcase.volume is not None:
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200143+ targets['1'] = {
144+ 'name': targetName(tcase.volume),
145+ 'volume': volumeName(tcase.volume),
146+ }
Peter Pentchevea354462023-07-18 11:15:56 +0300147+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200148+ return {
149+
150+ 'portalGroups': {
151+ '0': {
152+ 'name': _ISCSI_PORTAL_GROUP + '-not',
153+ 'networks': [],
154+ },
155+ '1': {
156+ 'name': _ISCSI_PORTAL_GROUP,
157+ 'networks': [
158+ {'address': "192.0.2.0"},
159+ {'address': "195.51.100.0"},
Peter Pentchevea354462023-07-18 11:15:56 +0300160+ ],
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200161+ },
Peter Pentchevea354462023-07-18 11:15:56 +0300162+ },
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200163+ 'initiators': initiators,
164+ 'targets': targets,
Peter Pentchevea354462023-07-18 11:15:56 +0300165+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200166+ }
Peter Pentchevea354462023-07-18 11:15:56 +0300167+
Peter Pentchevea354462023-07-18 11:15:56 +0300168+
169+
170+class MockIscsiAPI:
171+ """Mock only the iSCSI-related calls of the StorPool API bindings."""
172+
173+ _asrt: test.TestCase
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200174+ _configs: list[dict]
Peter Pentchevea354462023-07-18 11:15:56 +0300175+
176+ def __init__(
177+ self,
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200178+ configs: list[dict],
Peter Pentchevea354462023-07-18 11:15:56 +0300179+ asrt: test.TestCase,
180+ ) -> None:
181+ """Store the reference to the list of iSCSI config objects."""
182+ self._asrt = asrt
183+ self._configs = configs
184+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200185+ def get_iscsi_config(self) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300186+ """Return the last version of the iSCSI configuration."""
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200187+ return {'iscsi': self._configs[-1]}
Peter Pentchevea354462023-07-18 11:15:56 +0300188+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200189+ def _handle_export(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300190+ """Add an export for an initiator."""
191+ self._asrt.assertDictEqual(
192+ cmd,
193+ {
194+ 'initiator': _ISCSI_IQN_OURS,
195+ 'portalGroup': _ISCSI_PORTAL_GROUP,
Biser Milanovec3bf982024-11-27 17:42:17 +0200196+ 'volumeName': volumeName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300197+ },
198+ )
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200199+ self._asrt.assertEqual(cfg['initiators']['1']['name'], cmd['initiator'])
200+ self._asrt.assertListEqual(cfg['initiators']['1']['exports'], [])
Peter Pentchevea354462023-07-18 11:15:56 +0300201+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200202+ cfg['initiators'] = {
203+ **cfg['initiators'],
204+ '1': {
205+ **cfg['initiators']['1'],
206+ 'exports': [
207+ {
208+ 'portalGroup': cmd['portalGroup'],
209+ 'target': targetName(fake_constants.VOLUME_ID),
210+ },
211+ ],
Peter Pentchevea354462023-07-18 11:15:56 +0300212+ },
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200213+ }
214+ return cfg
Peter Pentchevea354462023-07-18 11:15:56 +0300215+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200216+ def _handle_delete_export(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200217+ """Delete an export for an initiator."""
218+ self._asrt.assertDictEqual(
219+ cmd,
220+ {
221+ 'initiator': _ISCSI_IQN_OURS,
222+ 'portalGroup': _ISCSI_PORTAL_GROUP,
Biser Milanovec3bf982024-11-27 17:42:17 +0200223+ 'volumeName': volumeName(fake_constants.VOLUME_ID),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200224+ },
225+ )
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200226+ self._asrt.assertEqual(cfg['initiators']['1']['name'], cmd['initiator'])
Biser Milanovd684c1c2024-11-22 12:12:59 +0200227+ self._asrt.assertListEqual(
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200228+ cfg['initiators']['1']['exports'],
229+ [{'portalGroup': _ISCSI_PORTAL_GROUP,
230+ 'target': cfg['targets']['1']['name']}])
Biser Milanovd684c1c2024-11-22 12:12:59 +0200231+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200232+ del cfg['initiators']['1']
233+ return cfg
Biser Milanovd684c1c2024-11-22 12:12:59 +0200234+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200235+ def _handle_create_initiator(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300236+ """Add a whole new initiator."""
237+ self._asrt.assertDictEqual(
238+ cmd,
239+ {
240+ 'name': _ISCSI_IQN_OURS,
241+ 'username': '',
242+ 'secret': '',
243+ },
244+ )
245+ self._asrt.assertNotIn(
246+ cmd['name'],
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200247+ [init['name'] for init in cfg['initiators'].values()],
Peter Pentchevea354462023-07-18 11:15:56 +0300248+ )
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200249+ self._asrt.assertListEqual(sorted(cfg['initiators']), ['0'])
Peter Pentchevea354462023-07-18 11:15:56 +0300250+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200251+ cfg['initiators'] = {
252+ **cfg['initiators'],
253+ '1': {'name': cmd['name'], 'exports': []},
254+ }
255+ return cfg
Peter Pentchevea354462023-07-18 11:15:56 +0300256+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200257+
258+ def _handle_create_target(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300259+ """Add a target for a volume so that it may be exported."""
260+ self._asrt.assertDictEqual(
261+ cmd,
Biser Milanovec3bf982024-11-27 17:42:17 +0200262+ {'volumeName': volumeName(fake_constants.VOLUME_ID)},
Peter Pentchevea354462023-07-18 11:15:56 +0300263+ )
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200264+ self._asrt.assertListEqual(sorted(cfg['targets']), ['0'])
265+ cfg['targets'] = {
266+ **cfg['targets'],
267+ '1': {
268+ 'name': targetName(fake_constants.VOLUME_ID),
269+ 'volume': volumeName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300270+ },
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200271+ }
272+ return cfg
Peter Pentchevea354462023-07-18 11:15:56 +0300273+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200274+ def _handle_delete_target(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200275+ """Remove a target for a volume."""
276+ self._asrt.assertDictEqual(
277+ cmd,
Biser Milanovec3bf982024-11-27 17:42:17 +0200278+ {'volumeName': volumeName(fake_constants.VOLUME_ID)},
Biser Milanovd684c1c2024-11-22 12:12:59 +0200279+ )
280+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200281+ self._asrt.assertListEqual(sorted(cfg['targets']), ['0', '1'])
282+ del cfg['targets']['1']
283+ return cfg
Biser Milanovd684c1c2024-11-22 12:12:59 +0200284+
Peter Pentchevea354462023-07-18 11:15:56 +0300285+ def _handle_initiator_add_network(
286+ self,
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200287+ cfg: dict,
Peter Pentchevea354462023-07-18 11:15:56 +0300288+ cmd: dict[str, Any],
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200289+ ) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300290+ """Add a network that an initiator is allowed to log in from."""
291+ self._asrt.assertDictEqual(
292+ cmd,
293+ {
294+ 'initiator': _ISCSI_IQN_OURS,
295+ 'net': '0.0.0.0/0',
296+ },
297+ )
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200298+ return cfg
Peter Pentchevea354462023-07-18 11:15:56 +0300299+
300+ _CMD_HANDLERS = {
301+ 'createInitiator': _handle_create_initiator,
302+ 'createTarget': _handle_create_target,
Biser Milanovd684c1c2024-11-22 12:12:59 +0200303+ 'deleteTarget': _handle_delete_target,
Peter Pentchevea354462023-07-18 11:15:56 +0300304+ 'export': _handle_export,
Biser Milanovd684c1c2024-11-22 12:12:59 +0200305+ 'exportDelete': _handle_delete_export,
Peter Pentchevea354462023-07-18 11:15:56 +0300306+ 'initiatorAddNetwork': _handle_initiator_add_network,
307+ }
308+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200309+ def post_iscsi_config(
Peter Pentchevea354462023-07-18 11:15:56 +0300310+ self,
311+ commands: dict[str, list[dict[str, dict[str, Any]]]],
312+ ) -> None:
313+ """Apply the requested changes to the iSCSI configuration.
314+
315+ This method adds a new config object to the configs list,
316+ making a shallow copy of the last one and applying the changes
317+ specified in the list of commands.
318+ """
319+ self._asrt.assertListEqual(sorted(commands), ['commands'])
320+ self._asrt.assertGreater(len(commands['commands']), 0)
321+ for cmd in commands['commands']:
322+ keys = sorted(cmd.keys())
323+ cmd_name = keys[0]
324+ self._asrt.assertListEqual(keys, [cmd_name])
325+ handler = self._CMD_HANDLERS[cmd_name]
326+ new_cfg = handler(self, self._configs[-1], cmd[cmd_name])
327+ self._configs.append(new_cfg)
328+
329+
330+_ISCSI_TEST_CASES = [
331+ IscsiTestCase(None, None, False, 4),
332+ IscsiTestCase(_ISCSI_IQN_OURS, None, False, 2),
Biser Milanovec3bf982024-11-27 17:42:17 +0200333+ IscsiTestCase(_ISCSI_IQN_OURS, fake_constants.VOLUME_ID, False, 1),
334+ IscsiTestCase(_ISCSI_IQN_OURS, fake_constants.VOLUME_ID, True, 0),
Peter Pentchevea354462023-07-18 11:15:56 +0300335+]
336+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200337
Peter Pentchevea354462023-07-18 11:15:56 +0300338 def MockSPConfig(section = 's01'):
339 res = {}
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200340@@ -198,7 +454,15 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300341 self.cfg.volume_backend_name = 'storpool_test'
342 self.cfg.storpool_template = None
343 self.cfg.storpool_replication = 3
Biser Milanovd684c1c2024-11-22 12:12:59 +0200344+ self.cfg.storpool_iscsi_cinder_volume = False
345+ self.cfg.storpool_iscsi_export_to = ''
346+ self.cfg.storpool_iscsi_learn_initiator_iqns = True
347+ self.cfg.storpool_iscsi_portal_group = _ISCSI_PORTAL_GROUP
Biser Milanovd684c1c2024-11-22 12:12:59 +0200348+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200349+ self._setup_test_driver()
350
Peter Pentchev9c24be92022-09-26 22:35:24 +0300351+ def _setup_test_driver(self):
352+ """Initialize a StorPool driver as per the current configuration."""
353 mock_exec = mock.Mock()
354 mock_exec.return_value = ('', '')
355
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200356@@ -216,7 +480,7 @@ class StorPoolTestCase(test.TestCase):
357 self.driver.check_for_setup_error()
Peter Pentchev9c24be92022-09-26 22:35:24 +0300358
359 @ddt.data(
360- (5, TypeError),
361+ (5, (TypeError, AttributeError)),
362 ({'no-host': None}, KeyError),
363 ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
364 ({'host': 's01'}, None),
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200365@@ -232,7 +496,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300366 conn)
367
368 @ddt.data(
369- (5, TypeError),
370+ (5, (TypeError, AttributeError)),
371 ({'no-host': None}, KeyError),
372 ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
373 )
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200374@@ -271,7 +535,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev5a9f8a62023-12-06 10:40:18 +0200375 self.assertEqual(21, pool['total_capacity_gb'])
376 self.assertEqual(5, int(pool['free_capacity_gb']))
377
378- self.assertTrue(pool['multiattach'])
379+ self.assertFalse(pool['multiattach'])
380 self.assertFalse(pool['QoS_support'])
381 self.assertFalse(pool['thick_provisioning_support'])
382 self.assertTrue(pool['thin_provisioning_support'])
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200383@@ -690,3 +954,179 @@ class StorPoolTestCase(test.TestCase):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200384 'No such volume',
385 self.driver.revert_to_snapshot, None,
386 {'id': vol_id}, {'id': snap_id})
Peter Pentchev9c24be92022-09-26 22:35:24 +0300387+
388+ @ddt.data(
389+ # The default values
Peter Pentchevea354462023-07-18 11:15:56 +0300390+ ('', False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300391+
392+ # Export to all
Peter Pentchevea354462023-07-18 11:15:56 +0300393+ ('*', True, constants.ISCSI, _ISCSI_IQN_OURS, True),
394+ ('*', True, constants.ISCSI, _ISCSI_IQN_OURS, True),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300395+
396+ # Only export to the controller
Peter Pentchevea354462023-07-18 11:15:56 +0300397+ ('', False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300398+
399+ # Some of the not-fully-supported pattern lists
Peter Pentchevea354462023-07-18 11:15:56 +0300400+ (_ISCSI_PAT_OTHER, False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
401+ (_ISCSI_PAT_OTHER, False, constants.STORPOOL, _ISCSI_IQN_OTHER, True),
402+ (_ISCSI_PAT_BOTH, False, constants.STORPOOL, _ISCSI_IQN_OURS, True),
403+ (_ISCSI_PAT_BOTH, False, constants.STORPOOL, _ISCSI_IQN_OTHER, True),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300404+ )
405+ @ddt.unpack
Biser Milanovd684c1c2024-11-22 12:12:59 +0200406+ def test_wants_iscsi(self, storpool_iscsi_export_to, use_iscsi,
407+ storage_protocol, hostname, expected):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300408+ """Check the "should this export use iSCSI?" detection."""
Biser Milanovd684c1c2024-11-22 12:12:59 +0200409+ self.cfg.storpool_iscsi_export_to = storpool_iscsi_export_to
Peter Pentchev9c24be92022-09-26 22:35:24 +0300410+ self._setup_test_driver()
411+ self.assertEqual(self.driver._use_iscsi, use_iscsi)
412+
413+ # Make sure the driver reports the correct protocol in the stats
414+ self.driver._update_volume_stats()
415+ self.assertEqual(self.driver._stats["vendor_name"], "StorPool")
416+ self.assertEqual(self.driver._stats["storage_protocol"],
417+ storage_protocol)
418+
419+ def check(conn, forced, expected):
420+ """Pass partially or completely valid connector info."""
421+ for initiator in (None, hostname):
Peter Pentchevea354462023-07-18 11:15:56 +0300422+ for host in (None, _ISCSI_IQN_THIRD):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300423+ self.assertEqual(
424+ self.driver._connector_wants_iscsi({
425+ "host": host,
426+ "initiator": initiator,
427+ **conn,
428+ }),
429+ expected if initiator is not None and host is not None
430+ else forced)
431+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200432+ # If storpool_iscsi_cinder_volume is set and this is the controller,
433+ # then yes.
Peter Pentchev9c24be92022-09-26 22:35:24 +0300434+ check({"storpool_wants_iscsi": True}, True, True)
435+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200436+ # If storpool_iscsi_cinder_volume is not set or this is not the
437+ # controller, then look at the specified expected value.
Peter Pentchev9c24be92022-09-26 22:35:24 +0300438+ check({"storpool_wants_iscsi": False}, use_iscsi, expected)
439+ check({}, use_iscsi, expected)
Peter Pentchevea354462023-07-18 11:15:56 +0300440+
441+ def _validate_iscsi_config(
442+ self,
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200443+ cfg: dict,
Peter Pentchevea354462023-07-18 11:15:56 +0300444+ res: dict[str, Any],
445+ tcase: IscsiTestCase,
446+ ) -> None:
447+ """Make sure the returned structure makes sense."""
448+ initiator = res['initiator']
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200449+ cfg_initiator = cfg['initiators'].get('1')
Peter Pentchevea354462023-07-18 11:15:56 +0300450+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200451+ self.assertIs(res['cfg']['iscsi'], cfg)
452+ self.assertEqual(res['pg']['name'], _ISCSI_PORTAL_GROUP)
Peter Pentchevea354462023-07-18 11:15:56 +0300453+
454+ if tcase.initiator is None:
455+ self.assertIsNone(initiator)
456+ else:
457+ self.assertIsNotNone(initiator)
458+ self.assertEqual(initiator, cfg_initiator)
459+
460+ if tcase.volume is None:
461+ self.assertIsNone(res['target'])
462+ else:
463+ self.assertIsNotNone(res['target'])
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200464+ self.assertEqual(res['target'], cfg['targets'].get('1'))
Peter Pentchevea354462023-07-18 11:15:56 +0300465+
466+ if tcase.initiator is None:
467+ self.assertIsNone(cfg_initiator)
468+ self.assertIsNone(res['export'])
469+ else:
470+ self.assertIsNotNone(cfg_initiator)
471+ if tcase.exported:
472+ self.assertIsNotNone(res['export'])
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200473+ self.assertEqual(res['export'], cfg_initiator['exports'][0])
Peter Pentchevea354462023-07-18 11:15:56 +0300474+ else:
475+ self.assertIsNone(res['export'])
476+
477+ @ddt.data(*_ISCSI_TEST_CASES)
478+ def test_iscsi_get_config(self, tcase: IscsiTestCase) -> None:
479+ """Make sure the StorPool iSCSI configuration is parsed correctly."""
480+ cfg_orig = MockIscsiConfig.build(tcase)
481+ configs = [cfg_orig]
482+ iapi = MockIscsiAPI(configs, self)
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200483+ with mock.patch.object(self.driver, '_sp_api', iapi):
Peter Pentchevea354462023-07-18 11:15:56 +0300484+ res = self.driver._get_iscsi_config(
485+ _ISCSI_IQN_OURS,
Biser Milanovec3bf982024-11-27 17:42:17 +0200486+ fake_constants.VOLUME_ID,
Peter Pentchevea354462023-07-18 11:15:56 +0300487+ )
488+
489+ self._validate_iscsi_config(cfg_orig, res, tcase)
490+
491+ @ddt.data(*_ISCSI_TEST_CASES)
492+ def test_iscsi_create_export(self, tcase: IscsiTestCase) -> None:
493+ """Make sure _create_iscsi_export() makes the right API calls."""
494+ cfg_orig = MockIscsiConfig.build(tcase)
495+ configs = [cfg_orig]
496+ iapi = MockIscsiAPI(configs, self)
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200497+ with mock.patch.object(self.driver, '_sp_api', iapi):
Peter Pentchevea354462023-07-18 11:15:56 +0300498+ self.driver._create_iscsi_export(
499+ {
Biser Milanovec3bf982024-11-27 17:42:17 +0200500+ 'id': fake_constants.VOLUME_ID,
501+ 'display_name': fake_constants.VOLUME_NAME,
Peter Pentchevea354462023-07-18 11:15:56 +0300502+ },
503+ {
504+ # Yeah, okay, so we cheat a little bit here...
505+ 'host': _ISCSI_IQN_OURS + '.hostname',
506+ 'initiator': _ISCSI_IQN_OURS,
507+ },
508+ )
509+
510+ self.assertEqual(len(configs), tcase.commands_count + 1)
511+ cfg_final = configs[-1]
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200512+ self.assertEqual(cfg_final['initiators']['1']['name'], _ISCSI_IQN_OURS)
Peter Pentchevea354462023-07-18 11:15:56 +0300513+ self.assertEqual(
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200514+ cfg_final['initiators']['1']['exports'][0]['target'],
Biser Milanovec3bf982024-11-27 17:42:17 +0200515+ targetName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300516+ )
517+ self.assertEqual(
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200518+ cfg_final['targets']['1']['volume'],
Biser Milanovec3bf982024-11-27 17:42:17 +0200519+ volumeName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300520+ )
Biser Milanovd684c1c2024-11-22 12:12:59 +0200521+
522+ @ddt.data(*_ISCSI_TEST_CASES)
523+ def test_remove_iscsi_export(self, tcase: IscsiTestCase):
524+ cfg_orig = MockIscsiConfig.build(tcase)
525+ configs = [cfg_orig]
526+ iapi = MockIscsiAPI(configs, self)
527+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200528+ def _target_exists(cfg: dict, volume: str) -> bool:
529+ for name, target in cfg['targets'].items():
530+ if target['volume'] == volumeName(volume):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200531+ return True
532+ return False
533+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200534+ def _export_exists(cfg: dict, volume: str) -> bool:
535+ for name, initiator in cfg['initiators'].items():
536+ for export in initiator['exports']:
537+ if export['target'] == targetName(volume):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200538+ return True
539+ return False
540+
541+ if tcase.exported:
542+ self.assertTrue(
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200543+ _target_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Biser Milanovd684c1c2024-11-22 12:12:59 +0200544+ self.assertTrue(
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200545+ _export_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Biser Milanovd684c1c2024-11-22 12:12:59 +0200546+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200547+ with mock.patch.object(self.driver, '_sp_api', iapi):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200548+ self.driver._remove_iscsi_export(
549+ {
Biser Milanovec3bf982024-11-27 17:42:17 +0200550+ 'id': fake_constants.VOLUME_ID,
551+ 'display_name': fake_constants.VOLUME_NAME,
Biser Milanovd684c1c2024-11-22 12:12:59 +0200552+ },
553+ {
554+ 'host': _ISCSI_IQN_OURS + '.hostname',
555+ 'initiator': _ISCSI_IQN_OURS,
556+ },
557+ )
558+
559+ self.assertFalse(
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200560+ _target_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Biser Milanovd684c1c2024-11-22 12:12:59 +0200561+ self.assertFalse(
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200562+ _export_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Peter Pentchevacaaa382023-02-28 11:26:13 +0200563diff --git a/cinder/volume/drivers/storpool.py b/cinder/volume/drivers/storpool.py
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200564index 2dc7bb6be..ec6f6ba50 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +0300565--- a/cinder/volume/drivers/storpool.py
566+++ b/cinder/volume/drivers/storpool.py
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200567@@ -15,13 +15,17 @@
Peter Pentchev9c24be92022-09-26 22:35:24 +0300568
Biser Milanovda1b0682024-11-29 09:37:10 +0200569 """StorPool block device driver"""
570
Peter Pentchev9c24be92022-09-26 22:35:24 +0300571+import fnmatch
Biser Milanovda1b0682024-11-29 09:37:10 +0200572 import platform
Biser Milanov90b5a142024-11-28 11:33:39 +0200573
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200574 from os_brick.initiator import storpool_utils
Biser Milanovda1b0682024-11-29 09:37:10 +0200575 from oslo_config import cfg
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200576 from oslo_log import log as logging
577 from oslo_utils import excutils
578+from oslo_utils import netutils
579 from oslo_utils import units
580+from oslo_utils import uuidutils
581+import six
582
583 from cinder.common import constants
584 from cinder import context
585@@ -36,6 +40,32 @@ LOG = logging.getLogger(__name__)
Peter Pentchev9c24be92022-09-26 22:35:24 +0300586
587
588 storpool_opts = [
Biser Milanovd684c1c2024-11-22 12:12:59 +0200589+ cfg.BoolOpt('storpool_iscsi_cinder_volume',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300590+ default=False,
591+ help='Let the cinder-volume service use iSCSI instead of '
592+ 'the StorPool block device driver for accessing '
593+ 'StorPool volumes, e.g. when creating a volume from '
594+ 'an image or vice versa.'),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200595+ cfg.StrOpt('storpool_iscsi_export_to',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300596+ default='',
597+ help='Whether to export volumes using iSCSI. '
598+ 'An empty string (the default) makes the driver export '
599+ 'all volumes using the StorPool native network protocol. '
600+ 'The value "*" makes the driver export all volumes using '
Biser Milanovd684c1c2024-11-22 12:12:59 +0200601+ 'iSCSI (see the Cinder StorPool driver documentation for '
602+ 'how this option and ``storpool_iscsi_cinder_volume`` '
603+ 'interact). Any other value leads to an experimental '
604+ 'not fully supported configuration and is interpreted as '
Peter Pentchev9c24be92022-09-26 22:35:24 +0300605+ 'a whitespace-separated list of patterns for IQNs for '
606+ 'hosts that need volumes to be exported via iSCSI, e.g. '
607+ '"iqn.1991-05.com.microsoft:\\*" for Windows hosts.'),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200608+ cfg.BoolOpt('storpool_iscsi_learn_initiator_iqns',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300609+ default=True,
610+ help='Create a StorPool record for a new initiator as soon as '
611+ 'Cinder asks for a volume to be exported to it.'),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200612+ cfg.StrOpt('storpool_iscsi_portal_group',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300613+ default=None,
614+ help='The portal group to export volumes via iSCSI in.'),
615 cfg.StrOpt('storpool_template',
616 default=None,
617 help='The StorPool template for volumes with no type.'),
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200618@@ -51,10 +81,38 @@ CONF = cfg.CONF
619 CONF.register_opts(storpool_opts, group=configuration.SHARED_CONF_GROUP)
620
621
622+def _extract_cinder_ids(urls):
623+ ids = []
624+ for url in urls:
625+ # The url can also be None and a TypeError is raised
626+ # TypeError: a bytes-like object is required, not 'str'
627+ if not url:
628+ continue
629+ parts = netutils.urlsplit(url)
630+ if parts.scheme == 'cinder':
631+ if parts.path:
632+ vol_id = parts.path.split('/')[-1]
633+ else:
634+ vol_id = parts.netloc
635+ if uuidutils.is_uuid_like(vol_id):
636+ ids.append(vol_id)
637+ else:
638+ LOG.debug("Ignoring malformed image location uri "
639+ "'%(url)s'", {'url': url})
640+
641+ return ids
642+
643+
644 class StorPoolConfigurationInvalid(exception.CinderException):
645 message = _("Invalid parameter %(param)s in the %(section)s section "
646 "of the /etc/storpool.conf file: %(error)s")
647
648+ def get_iscsi_config(self):
649+ return self._api_call('GET', '/ctrl/1.0/iSCSIConfig')
650+
651+ def post_iscsi_config(self, data):
652+ return self._api_call('POST', '/ctrl/1.0/iSCSIConfig', data)
653+
654
655 @interface.volumedriver
656 class StorPoolDriver(driver.VolumeDriver):
657@@ -89,10 +147,11 @@ class StorPoolDriver(driver.VolumeDriver):
658 2.1.0 - Use the new API client in os-brick to communicate with the
659 StorPool API instead of packages `storpool` and
660 `storpool.spopenstack`
661+ 2.2.0 - Add iSCSI export support.
662
Biser Milanovd684c1c2024-11-22 12:12:59 +0200663 """
664
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200665- VERSION = '2.1.0'
666+ VERSION = '2.2.0'
Biser Milanovd684c1c2024-11-22 12:12:59 +0200667 CI_WIKI_NAME = 'StorPool_distributed_storage_CI'
668
669 def __init__(self, *args, **kwargs):
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200670@@ -103,6 +162,7 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300671 self._ourIdInt = None
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200672 self._sp_api = None
673 self._volume_prefix = None
Biser Milanovd684c1c2024-11-22 12:12:59 +0200674+ self._use_iscsi = False
Peter Pentchev9c24be92022-09-26 22:35:24 +0300675
676 @staticmethod
677 def get_driver_options():
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200678@@ -158,10 +218,327 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300679 raise StorPoolConfigurationInvalid(
680 section=hostname, param='SP_OURID', error=e)
681
682+ def _connector_wants_iscsi(self, connector):
683+ """Should we do this export via iSCSI?
684+
685+ Check the configuration to determine whether this connector is
686+ expected to provide iSCSI exports as opposed to native StorPool
687+ protocol ones. Match the initiator's IQN against the list of
Biser Milanovd684c1c2024-11-22 12:12:59 +0200688+ patterns supplied in the "storpool_iscsi_export_to" configuration
689+ setting.
Peter Pentchev9c24be92022-09-26 22:35:24 +0300690+ """
691+ if connector is None:
692+ return False
693+ if self._use_iscsi:
694+ LOG.debug(' - forcing iSCSI for all exported volumes')
695+ return True
696+ if connector.get('storpool_wants_iscsi'):
697+ LOG.debug(' - forcing iSCSI for the controller')
698+ return True
699+
700+ try:
701+ iqn = connector.get('initiator')
702+ except Exception:
703+ iqn = None
704+ try:
705+ host = connector.get('host')
706+ except Exception:
707+ host = None
708+ if iqn is None or host is None:
709+ LOG.debug(' - this connector certainly does not want iSCSI')
710+ return False
711+
712+ LOG.debug(' - check whether %(host)s (%(iqn)s) wants iSCSI',
713+ {
714+ 'host': host,
715+ 'iqn': iqn,
716+ })
717+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200718+ export_to = self.configuration.storpool_iscsi_export_to
Peter Pentchev9c24be92022-09-26 22:35:24 +0300719+ if export_to is None:
720+ return False
721+
722+ for pat in export_to.split():
723+ LOG.debug(' - matching against %(pat)s', {'pat': pat})
724+ if fnmatch.fnmatch(iqn, pat):
725+ LOG.debug(' - got it!')
726+ return True
727+ LOG.debug(' - nope')
728+ return False
729+
730 def validate_connector(self, connector):
731+ if self._connector_wants_iscsi(connector):
732+ return True
733 return self._storpool_client_id(connector) >= 0
734
735+ def _get_iscsi_config(self, iqn, volume_id):
736+ """Get the StorPool iSCSI config items pertaining to this volume.
737+
738+ Find the elements of the StorPool iSCSI configuration tree that
739+ will be needed to create, ensure, or remove the iSCSI export of
740+ the specified volume to the specified initiator.
741+ """
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200742+ cfg = self._sp_api.get_iscsi_config()
Peter Pentchev9c24be92022-09-26 22:35:24 +0300743+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200744+ pg_name = self.configuration.storpool_iscsi_portal_group
Peter Pentchev9c24be92022-09-26 22:35:24 +0300745+ pg_found = [
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200746+ pg for pg in cfg['iscsi']['portalGroups'].values() if pg['name'] == pg_name
Peter Pentchev9c24be92022-09-26 22:35:24 +0300747+ ]
748+ if not pg_found:
749+ raise Exception('StorPool Cinder iSCSI configuration error: '
750+ 'no portal group "{pg}"'.format(pg=pg_name))
751+ pg = pg_found[0]
752+
753+ # Do we know about this initiator?
754+ i_found = [
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200755+ init for init in cfg['iscsi']['initiators'].values() if init['name'] == iqn
Peter Pentchev9c24be92022-09-26 22:35:24 +0300756+ ]
757+ if i_found:
758+ initiator = i_found[0]
759+ else:
760+ initiator = None
761+
762+ # Is this volume already being exported?
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200763+ volname = self._os_to_sp_volume_name(volume_id)
Peter Pentchev9c24be92022-09-26 22:35:24 +0300764+ t_found = [
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200765+ tgt for tgt in cfg['iscsi']['targets'].values() if tgt['volume'] == volname
Peter Pentchev9c24be92022-09-26 22:35:24 +0300766+ ]
767+ if t_found:
768+ target = t_found[0]
769+ else:
770+ target = None
771+
772+ # OK, so is this volume being exported to this initiator?
773+ export = None
774+ if initiator is not None and target is not None:
775+ e_found = [
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200776+ exp for exp in initiator['exports']
777+ if exp['portalGroup'] == pg['name'] and exp['target'] == target['name']
Peter Pentchev9c24be92022-09-26 22:35:24 +0300778+ ]
779+ if e_found:
780+ export = e_found[0]
781+
782+ return {
783+ 'cfg': cfg,
784+ 'pg': pg,
785+ 'initiator': initiator,
786+ 'target': target,
787+ 'export': export,
788+ 'volume_name': volname,
789+ 'volume_id': volume_id,
790+ }
791+
792+ def _create_iscsi_export(self, volume, connector):
793+ """Create (if needed) an iSCSI export for the StorPool volume."""
794+ LOG.debug(
795+ '_create_iscsi_export() invoked for volume '
796+ '"%(vol_name)s" (%(vol_id)s) connector %(connector)s',
797+ {
798+ 'vol_name': volume['display_name'],
799+ 'vol_id': volume['id'],
800+ 'connector': connector,
801+ }
802+ )
803+ iqn = connector['initiator']
804+ try:
805+ cfg = self._get_iscsi_config(iqn, volume['id'])
806+ except Exception as exc:
807+ LOG.error(
808+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
809+ )
810+ raise
811+
812+ if cfg['initiator'] is None:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200813+ if not (self.configuration.storpool_iscsi_learn_initiator_iqns or
814+ self.configuration.storpool_iscsi_cinder_volume and
Peter Pentchev9c24be92022-09-26 22:35:24 +0300815+ connector.get('storpool_wants_iscsi')):
816+ raise Exception('The "{iqn}" initiator IQN for the "{host}" '
817+ 'host is not defined in the StorPool '
818+ 'configuration.'
819+ .format(iqn=iqn, host=connector['host']))
820+ else:
821+ LOG.info('Creating a StorPool iSCSI initiator '
822+ 'for "{host}s" ({iqn}s)',
823+ {'host': connector['host'], 'iqn': iqn})
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200824+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300825+ 'commands': [
826+ {
827+ 'createInitiator': {
828+ 'name': iqn,
829+ 'username': '',
830+ 'secret': '',
831+ },
832+ },
833+ {
834+ 'initiatorAddNetwork': {
835+ 'initiator': iqn,
836+ 'net': '0.0.0.0/0',
837+ },
838+ },
839+ ]
840+ })
841+
842+ if cfg['target'] is None:
843+ LOG.info(
844+ 'Creating a StorPool iSCSI target '
845+ 'for the "%(vol_name)s" volume (%(vol_id)s)',
846+ {
847+ 'vol_name': volume['display_name'],
848+ 'vol_id': volume['id'],
849+ }
850+ )
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200851+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300852+ 'commands': [
853+ {
854+ 'createTarget': {
855+ 'volumeName': cfg['volume_name'],
856+ },
857+ },
858+ ]
859+ })
860+ cfg = self._get_iscsi_config(iqn, volume['id'])
861+
862+ if cfg['export'] is None:
863+ LOG.info('Creating a StorPool iSCSI export '
864+ 'for the "{vol_name}s" volume ({vol_id}s) '
865+ 'to the "{host}s" initiator ({iqn}s) '
866+ 'in the "{pg}s" portal group',
867+ {
868+ 'vol_name': volume['display_name'],
869+ 'vol_id': volume['id'],
870+ 'host': connector['host'],
871+ 'iqn': iqn,
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200872+ 'pg': cfg['pg']['name']
Peter Pentchev9c24be92022-09-26 22:35:24 +0300873+ })
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200874+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300875+ 'commands': [
876+ {
877+ 'export': {
878+ 'initiator': iqn,
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200879+ 'portalGroup': cfg['pg']['name'],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300880+ 'volumeName': cfg['volume_name'],
881+ },
882+ },
883+ ]
884+ })
885+
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200886+ target_portals = [
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200887+ "{addr}:3260".format(addr=net['address'])
888+ for net in cfg['pg']['networks']
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200889+ ]
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200890+ target_iqns = [cfg['target']['name']] * len(target_portals)
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200891+ target_luns = [0] * len(target_portals)
892+ if connector.get('multipath', False):
893+ multipath_settings = {
894+ 'target_iqns': target_iqns,
895+ 'target_portals': target_portals,
896+ 'target_luns': target_luns,
897+ }
898+ else:
899+ multipath_settings = {}
900+
Peter Pentchev9c24be92022-09-26 22:35:24 +0300901+ res = {
902+ 'driver_volume_type': 'iscsi',
903+ 'data': {
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200904+ **multipath_settings,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300905+ 'target_discovered': False,
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200906+ 'target_iqn': target_iqns[0],
907+ 'target_portal': target_portals[0],
908+ 'target_lun': target_luns[0],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300909+ 'volume_id': volume['id'],
910+ 'discard': True,
911+ },
912+ }
913+ LOG.debug('returning %(res)s', {'res': res})
914+ return res
915+
916+ def _remove_iscsi_export(self, volume, connector):
917+ """Remove an iSCSI export for the specified StorPool volume."""
918+ LOG.debug(
919+ '_remove_iscsi_export() invoked for volume '
920+ '"%(vol_name)s" (%(vol_id)s) connector %(conn)s',
921+ {
922+ 'vol_name': volume['display_name'],
923+ 'vol_id': volume['id'],
924+ 'conn': connector,
925+ }
926+ )
927+ try:
928+ cfg = self._get_iscsi_config(connector['initiator'], volume['id'])
929+ except Exception as exc:
930+ LOG.error(
931+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
932+ )
933+ raise
934+
935+ if cfg['export'] is not None:
936+ LOG.info('Removing the StorPool iSCSI export '
937+ 'for the "%(vol_name)s" volume (%(vol_id)s) '
938+ 'to the "%(host)s" initiator (%(iqn)s) '
939+ 'in the "%(pg)s" portal group',
940+ {
941+ 'vol_name': volume['display_name'],
942+ 'vol_id': volume['id'],
943+ 'host': connector['host'],
944+ 'iqn': connector['initiator'],
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200945+ 'pg': cfg['pg']['name'],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300946+ })
947+ try:
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200948+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300949+ 'commands': [
950+ {
951+ 'exportDelete': {
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200952+ 'initiator': cfg['initiator']['name'],
953+ 'portalGroup': cfg['pg']['name'],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300954+ 'volumeName': cfg['volume_name'],
955+ },
956+ },
957+ ]
958+ })
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200959+ except StorPoolAPIError as e:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300960+ if e.name not in ('objectExists', 'objectDoesNotExist'):
961+ raise
962+ LOG.info('Looks like somebody beat us to it')
963+
964+ if cfg['target'] is not None:
965+ last = True
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200966+ for initiator in cfg['cfg']['iscsi']['initiators'].values():
967+ if initiator['name'] == cfg['initiator']['name']:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300968+ continue
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200969+ for exp in initiator['exports']:
970+ if exp['target'] == cfg['target']['name']:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300971+ last = False
972+ break
973+ if not last:
974+ break
975+
976+ if last:
977+ LOG.info(
978+ 'Removing the StorPool iSCSI target '
979+ 'for the "{vol_name}s" volume ({vol_id}s)',
980+ {
981+ 'vol_name': volume['display_name'],
982+ 'vol_id': volume['id'],
983+ }
984+ )
985+ try:
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200986+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300987+ 'commands': [
988+ {
989+ 'deleteTarget': {
990+ 'volumeName': cfg['volume_name'],
991+ },
992+ },
993+ ]
994+ })
Biser Milanovbf1b0ea2025-01-20 16:36:46 +0200995+ except StorPoolAPIError as e:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300996+ if e.name not in ('objectDoesNotExist', 'invalidParam'):
997+ raise
998+ LOG.info('Looks like somebody beat us to it')
999+
1000 def initialize_connection(self, volume, connector):
1001+ if self._connector_wants_iscsi(connector):
1002+ return self._create_iscsi_export(volume, connector)
1003 return {'driver_volume_type': 'storpool',
1004 'data': {
1005 'client_id': self._storpool_client_id(connector),
Biser Milanovbf1b0ea2025-01-20 16:36:46 +02001006@@ -170,6 +547,9 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001007 }}
1008
1009 def terminate_connection(self, volume, connector, **kwargs):
1010+ if self._connector_wants_iscsi(connector):
1011+ LOG.debug('- removing an iSCSI export')
1012+ self._remove_iscsi_export(volume, connector)
1013 pass
1014
1015 def create_snapshot(self, snapshot):
Biser Milanovbf1b0ea2025-01-20 16:36:46 +02001016@@ -278,11 +658,20 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001017 )
1018
1019 def create_export(self, context, volume, connector):
1020- pass
1021+ if self._connector_wants_iscsi(connector):
1022+ LOG.debug('- creating an iSCSI export')
1023+ self._create_iscsi_export(volume, connector)
1024
1025 def remove_export(self, context, volume):
1026 pass
1027
1028+ def _attach_volume(self, context, volume, properties, remote=False):
Biser Milanovd684c1c2024-11-22 12:12:59 +02001029+ if self.configuration.storpool_iscsi_cinder_volume and not remote:
Peter Pentchev9c24be92022-09-26 22:35:24 +03001030+ LOG.debug('- adding the "storpool_wants_iscsi" flag')
1031+ properties['storpool_wants_iscsi'] = True
1032+
1033+ return super()._attach_volume(context, volume, properties, remote)
1034+
1035 def delete_volume(self, volume):
Biser Milanovbf1b0ea2025-01-20 16:36:46 +02001036 name = storpool_utils.os_to_sp_volume_name(
1037 self._volume_prefix, volume['id'])
1038@@ -321,6 +710,17 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001039 LOG.error("StorPoolDriver API initialization failed: %s", e)
1040 raise
1041
Biser Milanovd684c1c2024-11-22 12:12:59 +02001042+ export_to = self.configuration.storpool_iscsi_export_to
Peter Pentchev9c24be92022-09-26 22:35:24 +03001043+ export_to_set = export_to is not None and export_to.split()
Biser Milanovd684c1c2024-11-22 12:12:59 +02001044+ vol_iscsi = self.configuration.storpool_iscsi_cinder_volume
1045+ pg_name = self.configuration.storpool_iscsi_portal_group
Peter Pentchev9c24be92022-09-26 22:35:24 +03001046+ if (export_to_set or vol_iscsi) and pg_name is None:
Biser Milanovd684c1c2024-11-22 12:12:59 +02001047+ msg = _('The "storpool_iscsi_portal_group" option is required if '
1048+ 'any patterns are listed in "storpool_iscsi_export_to"')
Peter Pentchev9c24be92022-09-26 22:35:24 +03001049+ raise exception.VolumeDriverException(message=msg)
1050+
1051+ self._use_iscsi = export_to == "*"
1052+
1053 def _update_volume_stats(self):
1054 try:
Biser Milanovbf1b0ea2025-01-20 16:36:46 +02001055 dl = self._sp_api.disks_list()
1056@@ -346,7 +746,7 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001057 'total_capacity_gb': total / units.Gi,
1058 'free_capacity_gb': free / units.Gi,
1059 'reserved_percentage': 0,
1060- 'multiattach': True,
Peter Pentchev5a9f8a62023-12-06 10:40:18 +02001061+ 'multiattach': self._use_iscsi,
Peter Pentchev9c24be92022-09-26 22:35:24 +03001062 'QoS_support': False,
1063 'thick_provisioning_support': False,
1064 'thin_provisioning_support': True,
Biser Milanovbf1b0ea2025-01-20 16:36:46 +02001065@@ -365,7 +765,9 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001066 'volume_backend_name') or 'storpool',
1067 'vendor_name': 'StorPool',
1068 'driver_version': self.VERSION,
1069- 'storage_protocol': constants.STORPOOL,
1070+ 'storage_protocol': (
1071+ constants.ISCSI if self._use_iscsi else constants.STORPOOL
1072+ ),
Peter Pentchevacaaa382023-02-28 11:26:13 +02001073 # Driver capabilities
Peter Pentchev9c24be92022-09-26 22:35:24 +03001074 'clone_across_pools': True,
1075 'sparse_copy_volume': True,
Peter Pentchevacaaa382023-02-28 11:26:13 +02001076diff --git a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
Biser Milanovbf1b0ea2025-01-20 16:36:46 +02001077index d2c5895a9..1ba0d2862 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +03001078--- a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
1079+++ b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
Peter Pentchevacaaa382023-02-28 11:26:13 +02001080@@ -19,12 +19,15 @@ Prerequisites
Peter Pentchev9c24be92022-09-26 22:35:24 +03001081 * The controller and all the compute nodes must have access to the StorPool
1082 API service.
1083
1084-* All nodes where StorPool-backed volumes will be attached must have access to
1085+* If iSCSI is not being used as a transport (see below), all nodes where
1086+ StorPool-backed volumes will be attached must have access to
1087 the StorPool data network and run the ``storpool_block`` service.
1088
1089-* If StorPool-backed Cinder volumes need to be created directly from Glance
1090- images, then the node running the ``cinder-volume`` service must also have
1091- access to the StorPool data network and run the ``storpool_block`` service.
1092+* If Glance uses Cinder as its image store, or if StorPool-backed Cinder
1093+ volumes need to be created directly from Glance images, and iSCSI is not
1094+ being used as a transport, then the node running the ``cinder-volume``
1095+ service must also have access to the StorPool data network and run
1096+ the ``storpool_block`` service.
1097
1098 * All nodes that need to access the StorPool API (the compute nodes and
1099 the node running the ``cinder-volume`` service) must have the following
Biser Milanovbf1b0ea2025-01-20 16:36:46 +02001100@@ -34,6 +37,33 @@ Prerequisites
Peter Pentchev9c24be92022-09-26 22:35:24 +03001101 * the storpool Python bindings package
1102 * the storpool.spopenstack Python helper package
1103
1104+Using iSCSI as the transport protocol
1105+-------------------------------------
1106+
1107+The StorPool distributed storage system uses its own, highly optimized and
1108+tailored for its specifics, network protocol for communication between
1109+the storage servers and the clients (the OpenStack cluster nodes where
1110+StorPool-backed volumes will be attached). There are cases when granting
1111+various nodes access to the StorPool data network or installing and
1112+running the ``storpool_block`` client service on them may pose difficulties.
1113+The StorPool servers may also expose the user-created volumes and snapshots
1114+using the standard iSCSI protocol that only requires TCP routing and
1115+connectivity between the storage servers and the StorPool clients.
1116+The StorPool Cinder driver may be configured to export volumes and
Biser Milanovbf1b0ea2025-01-20 16:36:46 +02001117+snapshots via iSCSI using the ``iscsi_export_to`` and ``iscsi_portal_group``
1118+configuration options.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001119+
1120+Additionally, even if e.g. the hypervisor nodes running Nova will use
1121+the StorPool network protocol and run the ``storpool_block`` service
Biser Milanovbf1b0ea2025-01-20 16:36:46 +02001122+(so the ``iscsi_export_to`` option has its default empty string value),
1123+the ``iscsi_cinder_volume`` option configures the StorPool Cinder driver
1124+so that only the ``cinder-volume`` service will use the iSCSI protocol when
1125+attaching volumes and snapshots to transfer data to and from Glance images.
Biser Milanovd684c1c2024-11-22 12:12:59 +02001126+
1127+Multiattach support for StorPool is only enabled if iSCSI is used:
Biser Milanovbf1b0ea2025-01-20 16:36:46 +02001128+``iscsi_export_to`` is set to ``*``, that is, when all StorPool volumes
1129+will be exported via iSCSI to all initiators.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001130+
1131 Configuring the StorPool volume driver
1132 --------------------------------------
1133
Biser Milanovbf1b0ea2025-01-20 16:36:46 +02001134@@ -55,6 +85,32 @@ volume backend definition) and per volume type:
Peter Pentchev9c24be92022-09-26 22:35:24 +03001135 with the default placement constraints for the StorPool cluster.
1136 The default value for the chain replication is 3.
1137
Peter Pentchevea354462023-07-18 11:15:56 +03001138+In addition, if the iSCSI protocol is used to access the StorPool cluster as
1139+described in the previous section, the following options may be defined in
1140+the ``cinder.conf`` volume backend definition:
1141+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +02001142+- ``iscsi_export_to``: if set to the value ``*``, the StorPool Cinder driver
1143+ will export volumes and snapshots using the iSCSI protocol instead of
1144+ the StorPool network protocol. The ``iscsi_portal_group`` option must also
1145+ be specified.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001146+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +02001147+- ``iscsi_portal_group``: if the ``iscsi_export_to`` option is set to
1148+ the value ``*`` or the ``iscsi_cinder_volume`` option is turned on,
1149+ this option specifies the name of the iSCSI portal group that Cinder
1150+ volumes will be exported to.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001151+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +02001152+- ``iscsi_cinder_volume``: if enabled, even if the ``iscsi_export_to`` option
1153+ has its default empty value, the ``cinder-volume`` service will use iSCSI
1154+ to attach the volumes and snapshots for transferring data to and from
1155+ Glance images if Glance is configured to use the Cinder glance_store.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001156+
Biser Milanovbf1b0ea2025-01-20 16:36:46 +02001157+- ``iscsi_learn_initiator_iqns``: if enabled, the StorPool Cinder driver will
1158+ automatically use the StorPool API to create definitions for new initiators
1159+ in the StorPool cluster's configuration. This is the default behavior of
1160+ the driver; it may be disabled in the rare case if, e.g. because of site
1161+ policy, OpenStack iSCSI initiators (e.g. Nova hypervisors) need to be
1162+ explicitly allowed to use the StorPool iSCSI targets.
Peter Pentchevea354462023-07-18 11:15:56 +03001163+
Peter Pentchev9c24be92022-09-26 22:35:24 +03001164 Using the StorPool volume driver
1165 --------------------------------
1166