blob: 3b7ed42fc1fa8f14eb7f3996c66ba61ae85dfb52 [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
Biser Milanov90b5a142024-11-28 11:33:39 +02006Add iSCSI export support to the StorPool driver
7
Peter Pentchev9c24be92022-09-26 22:35:24 +03008Add four new driver options:
Biser Milanovd684c1c2024-11-22 12:12:59 +02009- storpool_iscsi_cinder_volume: use StorPool iSCSI attachments whenever
Peter Pentchev9c24be92022-09-26 22:35:24 +030010 the cinder-volume service needs to attach a volume to the controller,
11 e.g. for copying an image to a volume or vice versa
Biser Milanovd684c1c2024-11-22 12:12:59 +020012- storpool_iscsi_export_to:
13 - an empty string to use the StorPool native protocol for exporting
14 volumes
Peter Pentchev9c24be92022-09-26 22:35:24 +030015 - the string "*" to always use iSCSI for exporting volumes
16 - an experimental, not fully supported list of IQN patterns to export
17 volumes to using iSCSI; this results in a Cinder driver that exports
18 different volumes using different storage protocols
Biser Milanovd684c1c2024-11-22 12:12:59 +020019- storpool_iscsi_portal_group: the name of the iSCSI portal group
20 defined in the StorPool configuration to use for these export
21- storpool_iscsi_learn_initiator_iqns: automatically create StorPool
22 configuration records for an initiator when a volume is first exported
23 to it
Peter Pentchev9c24be92022-09-26 22:35:24 +030024
Biser Milanovd684c1c2024-11-22 12:12:59 +020025When exporting volumes via iSCSI, report the storage protocol as
26"iSCSI".
Peter Pentchev9c24be92022-09-26 22:35:24 +030027
28Change-Id: I9de64306e0e6976268df782053b0651dd1cca96f
29---
Biser Milanov90b5a142024-11-28 11:33:39 +020030 .../unit/volume/drivers/test_storpool.py | 438 +++++++++++++++++-
31 cinder/volume/drivers/storpool.py | 410 +++++++++++++++-
32 .../drivers/storpool-volume-driver.rst | 64 ++-
33 3 files changed, 901 insertions(+), 11 deletions(-)
Peter Pentchev9c24be92022-09-26 22:35:24 +030034
Peter Pentchevacaaa382023-02-28 11:26:13 +020035diff --git a/cinder/tests/unit/volume/drivers/test_storpool.py b/cinder/tests/unit/volume/drivers/test_storpool.py
Biser Milanov90b5a142024-11-28 11:33:39 +020036index cee3fdd67..3dd84ec4a 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +030037--- a/cinder/tests/unit/volume/drivers/test_storpool.py
38+++ b/cinder/tests/unit/volume/drivers/test_storpool.py
Biser Milanov90b5a142024-11-28 11:33:39 +020039@@ -15,14 +15,17 @@
Peter Pentchevea354462023-07-18 11:15:56 +030040
41
Biser Milanov90b5a142024-11-28 11:33:39 +020042 import copy
Peter Pentchevea354462023-07-18 11:15:56 +030043+import dataclasses
44 import itertools
Biser Milanov90b5a142024-11-28 11:33:39 +020045 import os
Peter Pentchevea354462023-07-18 11:15:56 +030046 import re
Peter Pentchevea354462023-07-18 11:15:56 +030047+from typing import Any, NamedTuple, TYPE_CHECKING # noqa: H301
48 from unittest import mock
49
50 import ddt
51 from oslo_utils import units
Peter Pentchevea354462023-07-18 11:15:56 +030052
Peter Pentchev9c24be92022-09-26 22:35:24 +030053+from cinder.common import constants
54 from cinder import exception
Biser Milanovec3bf982024-11-27 17:42:17 +020055 from cinder.tests.unit import fake_constants
Peter Pentchev9c24be92022-09-26 22:35:24 +030056 from cinder.tests.unit import test
Biser Milanov90b5a142024-11-28 11:33:39 +020057@@ -56,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 Milanov90b5a142024-11-28 11:33:39 +020067
Peter Pentchevea354462023-07-18 11:15:56 +030068 volume_types = {
Biser Milanovec3bf982024-11-27 17:42:17 +020069 fake_constants.VOLUME_TYPE_ID: {},
Biser Milanov90b5a142024-11-28 11:33:39 +020070@@ -94,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 Milanov90b5a142024-11-28 11:33:39 +020078 class MockAPI(object):
79 def __init__(self, *args):
80 self._disks = {}
81@@ -204,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 Milanov90b5a142024-11-28 11:33:39 +020099+ def build(cls, tcase: IscsiTestCase) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300100+ """Build a test config structure."""
101+ initiators = {
Biser Milanov90b5a142024-11-28 11:33:39 +0200102+ '0': {'name': _ISCSI_IQN_OTHER, 'exports': []},
Peter Pentchevea354462023-07-18 11:15:56 +0300103+ }
104+ if tcase.initiator is not None:
Biser Milanov90b5a142024-11-28 11:33:39 +0200105+ initiators['1'] = {
106+ 'name': tcase.initiator,
107+ 'exports': (
Peter Pentchevea354462023-07-18 11:15:56 +0300108+ [
Biser Milanov90b5a142024-11-28 11:33:39 +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 Milanov90b5a142024-11-28 11:33:39 +0200117+ }
Peter Pentchevea354462023-07-18 11:15:56 +0300118+
119+ targets = {
Biser Milanov90b5a142024-11-28 11:33:39 +0200120+ '0': {
121+ 'name': targetName(fake_constants.VOLUME2_ID),
122+ 'volume': volumeName(fake_constants.VOLUME2_ID),
123+ },
Peter Pentchevea354462023-07-18 11:15:56 +0300124+ }
125+ if tcase.volume is not None:
Biser Milanov90b5a142024-11-28 11:33:39 +0200126+ targets['1'] = {
127+ 'name': targetName(tcase.volume),
128+ 'volume': volumeName(tcase.volume),
129+ }
Peter Pentchevea354462023-07-18 11:15:56 +0300130+
Biser Milanov90b5a142024-11-28 11:33:39 +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 Milanov90b5a142024-11-28 11:33:39 +0200144+ },
Peter Pentchevea354462023-07-18 11:15:56 +0300145+ },
Biser Milanov90b5a142024-11-28 11:33:39 +0200146+ 'initiators': initiators,
147+ 'targets': targets,
Peter Pentchevea354462023-07-18 11:15:56 +0300148+
Biser Milanov90b5a142024-11-28 11:33:39 +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 Milanov90b5a142024-11-28 11:33:39 +0200157+ _configs: list[dict]
Peter Pentchevea354462023-07-18 11:15:56 +0300158+
159+ def __init__(
160+ self,
Biser Milanov90b5a142024-11-28 11:33:39 +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 Milanov90b5a142024-11-28 11:33:39 +0200168+ def get_iscsi_config(self) -> dict:
Peter Pentchevea354462023-07-18 11:15:56 +0300169+ """Return the last version of the iSCSI configuration."""
Biser Milanov90b5a142024-11-28 11:33:39 +0200170+ return {'iscsi': self._configs[-1]}
Peter Pentchevea354462023-07-18 11:15:56 +0300171+
Biser Milanov90b5a142024-11-28 11:33:39 +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,
Biser Milanovec3bf982024-11-27 17:42:17 +0200179+ 'volumeName': volumeName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300180+ },
181+ )
Biser Milanov90b5a142024-11-28 11:33:39 +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 Milanov90b5a142024-11-28 11:33:39 +0200185+ cfg['initiators'] = {
186+ **cfg['initiators'],
187+ '1': {
188+ **cfg['initiators']['1'],
189+ 'exports': [
190+ {
191+ 'portalGroup': cmd['portalGroup'],
192+ 'target': targetName(fake_constants.VOLUME_ID),
193+ },
194+ ],
Peter Pentchevea354462023-07-18 11:15:56 +0300195+ },
Biser Milanov90b5a142024-11-28 11:33:39 +0200196+ }
197+ return cfg
Peter Pentchevea354462023-07-18 11:15:56 +0300198+
Biser Milanov90b5a142024-11-28 11:33:39 +0200199+ def _handle_delete_export(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200200+ """Delete an export for an initiator."""
201+ self._asrt.assertDictEqual(
202+ cmd,
203+ {
204+ 'initiator': _ISCSI_IQN_OURS,
205+ 'portalGroup': _ISCSI_PORTAL_GROUP,
Biser Milanovec3bf982024-11-27 17:42:17 +0200206+ 'volumeName': volumeName(fake_constants.VOLUME_ID),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200207+ },
208+ )
Biser Milanov90b5a142024-11-28 11:33:39 +0200209+ self._asrt.assertEqual(cfg['initiators']['1']['name'], cmd['initiator'])
Biser Milanovd684c1c2024-11-22 12:12:59 +0200210+ self._asrt.assertListEqual(
Biser Milanov90b5a142024-11-28 11:33:39 +0200211+ cfg['initiators']['1']['exports'],
212+ [{'portalGroup': _ISCSI_PORTAL_GROUP,
213+ 'target': cfg['targets']['1']['name']}])
Biser Milanovd684c1c2024-11-22 12:12:59 +0200214+
Biser Milanov90b5a142024-11-28 11:33:39 +0200215+ del cfg['initiators']['1']
216+ return cfg
Biser Milanovd684c1c2024-11-22 12:12:59 +0200217+
Biser Milanov90b5a142024-11-28 11:33:39 +0200218+ 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 Milanov90b5a142024-11-28 11:33:39 +0200230+ [init['name'] for init in cfg['initiators'].values()],
Peter Pentchevea354462023-07-18 11:15:56 +0300231+ )
Biser Milanov90b5a142024-11-28 11:33:39 +0200232+ self._asrt.assertListEqual(sorted(cfg['initiators']), ['0'])
Peter Pentchevea354462023-07-18 11:15:56 +0300233+
Biser Milanov90b5a142024-11-28 11:33:39 +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 Milanov90b5a142024-11-28 11:33:39 +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,
Biser Milanovec3bf982024-11-27 17:42:17 +0200245+ {'volumeName': volumeName(fake_constants.VOLUME_ID)},
Peter Pentchevea354462023-07-18 11:15:56 +0300246+ )
Biser Milanov90b5a142024-11-28 11:33:39 +0200247+ self._asrt.assertListEqual(sorted(cfg['targets']), ['0'])
248+ cfg['targets'] = {
249+ **cfg['targets'],
250+ '1': {
251+ 'name': targetName(fake_constants.VOLUME_ID),
252+ 'volume': volumeName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300253+ },
Biser Milanov90b5a142024-11-28 11:33:39 +0200254+ }
255+ return cfg
Peter Pentchevea354462023-07-18 11:15:56 +0300256+
Biser Milanov90b5a142024-11-28 11:33:39 +0200257+ def _handle_delete_target(self, cfg: dict, cmd: dict[str, Any]) -> dict:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200258+ """Remove a target for a volume."""
259+ self._asrt.assertDictEqual(
260+ cmd,
Biser Milanovec3bf982024-11-27 17:42:17 +0200261+ {'volumeName': volumeName(fake_constants.VOLUME_ID)},
Biser Milanovd684c1c2024-11-22 12:12:59 +0200262+ )
263+
Biser Milanov90b5a142024-11-28 11:33:39 +0200264+ self._asrt.assertListEqual(sorted(cfg['targets']), ['0', '1'])
265+ del cfg['targets']['1']
266+ return cfg
Biser Milanovd684c1c2024-11-22 12:12:59 +0200267+
Peter Pentchevea354462023-07-18 11:15:56 +0300268+ def _handle_initiator_add_network(
269+ self,
Biser Milanov90b5a142024-11-28 11:33:39 +0200270+ cfg: dict,
Peter Pentchevea354462023-07-18 11:15:56 +0300271+ cmd: dict[str, Any],
Biser Milanov90b5a142024-11-28 11:33:39 +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 Milanov90b5a142024-11-28 11:33:39 +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 Milanovd684c1c2024-11-22 12:12:59 +0200286+ 'deleteTarget': _handle_delete_target,
Peter Pentchevea354462023-07-18 11:15:56 +0300287+ 'export': _handle_export,
Biser Milanovd684c1c2024-11-22 12:12:59 +0200288+ 'exportDelete': _handle_delete_export,
Peter Pentchevea354462023-07-18 11:15:56 +0300289+ 'initiatorAddNetwork': _handle_initiator_add_network,
290+ }
291+
Biser Milanov90b5a142024-11-28 11:33:39 +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),
Biser Milanovec3bf982024-11-27 17:42:17 +0200316+ IscsiTestCase(_ISCSI_IQN_OURS, fake_constants.VOLUME_ID, False, 1),
317+ IscsiTestCase(_ISCSI_IQN_OURS, fake_constants.VOLUME_ID, True, 0),
Peter Pentchevea354462023-07-18 11:15:56 +0300318+]
319+
Biser Milanov90b5a142024-11-28 11:33:39 +0200320
Peter Pentchevea354462023-07-18 11:15:56 +0300321 def MockSPConfig(section = 's01'):
322 res = {}
Biser Milanov90b5a142024-11-28 11:33:39 +0200323@@ -383,7 +631,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
Biser Milanovd684c1c2024-11-22 12:12:59 +0200327+ self.cfg.storpool_iscsi_cinder_volume = False
328+ self.cfg.storpool_iscsi_export_to = ''
329+ self.cfg.storpool_iscsi_learn_initiator_iqns = True
330+ self.cfg.storpool_iscsi_portal_group = _ISCSI_PORTAL_GROUP
Peter Pentchevea354462023-07-18 11:15:56 +0300331
Biser Milanovd684c1c2024-11-22 12:12:59 +0200332+ 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 Milanov90b5a142024-11-28 11:33:39 +0200339@@ -400,7 +656,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 Milanov90b5a142024-11-28 11:33:39 +0200348@@ -416,7 +672,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 Milanov90b5a142024-11-28 11:33:39 +0200357@@ -455,7 +711,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 Milanov90b5a142024-11-28 11:33:39 +0200366@@ -874,3 +1130,179 @@ class StorPoolTestCase(test.TestCase):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200367 '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
Biser Milanovd684c1c2024-11-22 12:12:59 +0200389+ def test_wants_iscsi(self, storpool_iscsi_export_to, use_iscsi,
390+ storage_protocol, hostname, expected):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300391+ """Check the "should this export use iSCSI?" detection."""
Biser Milanovd684c1c2024-11-22 12:12:59 +0200392+ self.cfg.storpool_iscsi_export_to = storpool_iscsi_export_to
Peter Pentchev9c24be92022-09-26 22:35:24 +0300393+ 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+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200415+ # If storpool_iscsi_cinder_volume is set and this is the controller,
416+ # then yes.
Peter Pentchev9c24be92022-09-26 22:35:24 +0300417+ check({"storpool_wants_iscsi": True}, True, True)
418+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200419+ # If storpool_iscsi_cinder_volume is not set or this is not the
420+ # controller, then look at the specified expected value.
Peter Pentchev9c24be92022-09-26 22:35:24 +0300421+ check({"storpool_wants_iscsi": False}, use_iscsi, expected)
422+ check({}, use_iscsi, expected)
Peter Pentchevea354462023-07-18 11:15:56 +0300423+
424+ def _validate_iscsi_config(
425+ self,
Biser Milanov90b5a142024-11-28 11:33:39 +0200426+ cfg: dict,
Peter Pentchevea354462023-07-18 11:15:56 +0300427+ res: dict[str, Any],
428+ tcase: IscsiTestCase,
429+ ) -> None:
430+ """Make sure the returned structure makes sense."""
431+ initiator = res['initiator']
Biser Milanov90b5a142024-11-28 11:33:39 +0200432+ cfg_initiator = cfg['initiators'].get('1')
Peter Pentchevea354462023-07-18 11:15:56 +0300433+
Biser Milanov90b5a142024-11-28 11:33:39 +0200434+ self.assertIs(res['cfg']['iscsi'], cfg)
435+ self.assertEqual(res['pg']['name'], _ISCSI_PORTAL_GROUP)
Peter Pentchevea354462023-07-18 11:15:56 +0300436+
437+ if tcase.initiator is None:
438+ self.assertIsNone(initiator)
439+ else:
440+ self.assertIsNotNone(initiator)
441+ self.assertEqual(initiator, cfg_initiator)
442+
443+ if tcase.volume is None:
444+ self.assertIsNone(res['target'])
445+ else:
446+ self.assertIsNotNone(res['target'])
Biser Milanov90b5a142024-11-28 11:33:39 +0200447+ self.assertEqual(res['target'], cfg['targets'].get('1'))
Peter Pentchevea354462023-07-18 11:15:56 +0300448+
449+ if tcase.initiator is None:
450+ self.assertIsNone(cfg_initiator)
451+ self.assertIsNone(res['export'])
452+ else:
453+ self.assertIsNotNone(cfg_initiator)
454+ if tcase.exported:
455+ self.assertIsNotNone(res['export'])
Biser Milanov90b5a142024-11-28 11:33:39 +0200456+ self.assertEqual(res['export'], cfg_initiator['exports'][0])
Peter Pentchevea354462023-07-18 11:15:56 +0300457+ else:
458+ self.assertIsNone(res['export'])
459+
460+ @ddt.data(*_ISCSI_TEST_CASES)
461+ def test_iscsi_get_config(self, tcase: IscsiTestCase) -> None:
462+ """Make sure the StorPool iSCSI configuration is parsed correctly."""
463+ cfg_orig = MockIscsiConfig.build(tcase)
464+ configs = [cfg_orig]
465+ iapi = MockIscsiAPI(configs, self)
Biser Milanov90b5a142024-11-28 11:33:39 +0200466+ with mock.patch.object(self.driver, '_sp_api', iapi):
Peter Pentchevea354462023-07-18 11:15:56 +0300467+ res = self.driver._get_iscsi_config(
468+ _ISCSI_IQN_OURS,
Biser Milanovec3bf982024-11-27 17:42:17 +0200469+ fake_constants.VOLUME_ID,
Peter Pentchevea354462023-07-18 11:15:56 +0300470+ )
471+
472+ self._validate_iscsi_config(cfg_orig, res, tcase)
473+
474+ @ddt.data(*_ISCSI_TEST_CASES)
475+ def test_iscsi_create_export(self, tcase: IscsiTestCase) -> None:
476+ """Make sure _create_iscsi_export() makes the right API calls."""
477+ cfg_orig = MockIscsiConfig.build(tcase)
478+ configs = [cfg_orig]
479+ iapi = MockIscsiAPI(configs, self)
Biser Milanov90b5a142024-11-28 11:33:39 +0200480+ with mock.patch.object(self.driver, '_sp_api', iapi):
Peter Pentchevea354462023-07-18 11:15:56 +0300481+ self.driver._create_iscsi_export(
482+ {
Biser Milanovec3bf982024-11-27 17:42:17 +0200483+ 'id': fake_constants.VOLUME_ID,
484+ 'display_name': fake_constants.VOLUME_NAME,
Peter Pentchevea354462023-07-18 11:15:56 +0300485+ },
486+ {
487+ # Yeah, okay, so we cheat a little bit here...
488+ 'host': _ISCSI_IQN_OURS + '.hostname',
489+ 'initiator': _ISCSI_IQN_OURS,
490+ },
491+ )
492+
493+ self.assertEqual(len(configs), tcase.commands_count + 1)
494+ cfg_final = configs[-1]
Biser Milanov90b5a142024-11-28 11:33:39 +0200495+ self.assertEqual(cfg_final['initiators']['1']['name'], _ISCSI_IQN_OURS)
Peter Pentchevea354462023-07-18 11:15:56 +0300496+ self.assertEqual(
Biser Milanov90b5a142024-11-28 11:33:39 +0200497+ cfg_final['initiators']['1']['exports'][0]['target'],
Biser Milanovec3bf982024-11-27 17:42:17 +0200498+ targetName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300499+ )
500+ self.assertEqual(
Biser Milanov90b5a142024-11-28 11:33:39 +0200501+ cfg_final['targets']['1']['volume'],
Biser Milanovec3bf982024-11-27 17:42:17 +0200502+ volumeName(fake_constants.VOLUME_ID),
Peter Pentchevea354462023-07-18 11:15:56 +0300503+ )
Biser Milanovd684c1c2024-11-22 12:12:59 +0200504+
505+ @ddt.data(*_ISCSI_TEST_CASES)
506+ def test_remove_iscsi_export(self, tcase: IscsiTestCase):
507+ cfg_orig = MockIscsiConfig.build(tcase)
508+ configs = [cfg_orig]
509+ iapi = MockIscsiAPI(configs, self)
510+
Biser Milanov90b5a142024-11-28 11:33:39 +0200511+ def _target_exists(cfg: dict, volume: str) -> bool:
512+ for name, target in cfg['targets'].items():
513+ if target['volume'] == volumeName(volume):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200514+ return True
515+ return False
516+
Biser Milanov90b5a142024-11-28 11:33:39 +0200517+ def _export_exists(cfg: dict, volume: str) -> bool:
518+ for name, initiator in cfg['initiators'].items():
519+ for export in initiator['exports']:
520+ if export['target'] == targetName(volume):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200521+ return True
522+ return False
523+
524+ if tcase.exported:
525+ self.assertTrue(
Biser Milanov90b5a142024-11-28 11:33:39 +0200526+ _target_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Biser Milanovd684c1c2024-11-22 12:12:59 +0200527+ self.assertTrue(
Biser Milanov90b5a142024-11-28 11:33:39 +0200528+ _export_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Biser Milanovd684c1c2024-11-22 12:12:59 +0200529+
Biser Milanov90b5a142024-11-28 11:33:39 +0200530+ with mock.patch.object(self.driver, '_sp_api', iapi):
Biser Milanovd684c1c2024-11-22 12:12:59 +0200531+ self.driver._remove_iscsi_export(
532+ {
Biser Milanovec3bf982024-11-27 17:42:17 +0200533+ 'id': fake_constants.VOLUME_ID,
534+ 'display_name': fake_constants.VOLUME_NAME,
Biser Milanovd684c1c2024-11-22 12:12:59 +0200535+ },
536+ {
537+ 'host': _ISCSI_IQN_OURS + '.hostname',
538+ 'initiator': _ISCSI_IQN_OURS,
539+ },
540+ )
541+
542+ self.assertFalse(
Biser Milanov90b5a142024-11-28 11:33:39 +0200543+ _target_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Biser Milanovd684c1c2024-11-22 12:12:59 +0200544+ self.assertFalse(
Biser Milanov90b5a142024-11-28 11:33:39 +0200545+ _export_exists(iapi.get_iscsi_config()['iscsi'], tcase.volume))
Peter Pentchevacaaa382023-02-28 11:26:13 +0200546diff --git a/cinder/volume/drivers/storpool.py b/cinder/volume/drivers/storpool.py
Biser Milanov90b5a142024-11-28 11:33:39 +0200547index 1707427fa..6a6f5b2e5 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +0300548--- a/cinder/volume/drivers/storpool.py
549+++ b/cinder/volume/drivers/storpool.py
Biser Milanov90b5a142024-11-28 11:33:39 +0200550@@ -17,6 +17,7 @@
Peter Pentchev9c24be92022-09-26 22:35:24 +0300551
Biser Milanov90b5a142024-11-28 11:33:39 +0200552 import configparser
553 import errno
Peter Pentchev9c24be92022-09-26 22:35:24 +0300554+import fnmatch
Biser Milanov90b5a142024-11-28 11:33:39 +0200555 import http.client
556 import json
557 import os
558@@ -28,7 +29,10 @@ import time
Biser Milanov8323a8c2024-11-20 10:53:27 +0200559 from oslo_config import cfg
Biser Milanov90b5a142024-11-28 11:33:39 +0200560 from oslo_log import log as logging
561 from oslo_utils import excutils
562+from oslo_utils import netutils
563 from oslo_utils import units
564+from oslo_utils import uuidutils
565+import six
566
567 from cinder.common import constants
568 from cinder import context
569@@ -46,6 +50,32 @@ DEV_STORPOOL_BYID = pathlib.Path('/dev/storpool-byid')
Peter Pentchev9c24be92022-09-26 22:35:24 +0300570
571
572 storpool_opts = [
Biser Milanovd684c1c2024-11-22 12:12:59 +0200573+ cfg.BoolOpt('storpool_iscsi_cinder_volume',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300574+ default=False,
575+ help='Let the cinder-volume service use iSCSI instead of '
576+ 'the StorPool block device driver for accessing '
577+ 'StorPool volumes, e.g. when creating a volume from '
578+ 'an image or vice versa.'),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200579+ cfg.StrOpt('storpool_iscsi_export_to',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300580+ default='',
581+ help='Whether to export volumes using iSCSI. '
582+ 'An empty string (the default) makes the driver export '
583+ 'all volumes using the StorPool native network protocol. '
584+ 'The value "*" makes the driver export all volumes using '
Biser Milanovd684c1c2024-11-22 12:12:59 +0200585+ 'iSCSI (see the Cinder StorPool driver documentation for '
586+ 'how this option and ``storpool_iscsi_cinder_volume`` '
587+ 'interact). Any other value leads to an experimental '
588+ 'not fully supported configuration and is interpreted as '
Peter Pentchev9c24be92022-09-26 22:35:24 +0300589+ 'a whitespace-separated list of patterns for IQNs for '
590+ 'hosts that need volumes to be exported via iSCSI, e.g. '
591+ '"iqn.1991-05.com.microsoft:\\*" for Windows hosts.'),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200592+ cfg.BoolOpt('storpool_iscsi_learn_initiator_iqns',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300593+ default=True,
594+ help='Create a StorPool record for a new initiator as soon as '
595+ 'Cinder asks for a volume to be exported to it.'),
Biser Milanovd684c1c2024-11-22 12:12:59 +0200596+ cfg.StrOpt('storpool_iscsi_portal_group',
Peter Pentchev9c24be92022-09-26 22:35:24 +0300597+ default=None,
598+ help='The portal group to export volumes via iSCSI in.'),
599 cfg.StrOpt('storpool_template',
600 default=None,
601 help='The StorPool template for volumes with no type.'),
Biser Milanov90b5a142024-11-28 11:33:39 +0200602@@ -61,6 +91,28 @@ CONF = cfg.CONF
603 CONF.register_opts(storpool_opts, group=configuration.SHARED_CONF_GROUP)
604
605
606+def _extract_cinder_ids(urls):
607+ ids = []
608+ for url in urls:
609+ # The url can also be None and a TypeError is raised
610+ # TypeError: a bytes-like object is required, not 'str'
611+ if not url:
612+ continue
613+ parts = netutils.urlsplit(url)
614+ if parts.scheme == 'cinder':
615+ if parts.path:
616+ vol_id = parts.path.split('/')[-1]
617+ else:
618+ vol_id = parts.netloc
619+ if uuidutils.is_uuid_like(vol_id):
620+ ids.append(vol_id)
621+ else:
622+ LOG.debug("Ignoring malformed image location uri "
623+ "'%(url)s'", {'url': url})
624+
625+ return ids
626+
627+
628 class StorPoolConfigurationInvalid(exception.CinderException):
629 message = _("Invalid parameter %(param)s in the %(section)s section "
630 "of the /etc/storpool.conf file: %(error)s")
631@@ -233,6 +285,12 @@ class StorPoolAPI:
632 self._api_call(
633 'POST', f'/ctrl/1.0/MultiCluster/SnapshotDelete/{snapshot}')
634
635+ def get_iscsi_config(self):
636+ return self._api_call('GET', '/ctrl/1.0/iSCSIConfig')
637+
638+ def post_iscsi_config(self, data):
639+ return self._api_call('POST', '/ctrl/1.0/iSCSIConfig', data)
640+
641
642 @interface.volumedriver
643 class StorPoolDriver(driver.VolumeDriver):
644@@ -267,9 +325,10 @@ class StorPoolDriver(driver.VolumeDriver):
645 2.1.0 - Use a new in-tree API client to communicate with the
646 StorPool API instead of packages `storpool` and
647 `storpool.spopenstack`
648+ 2.2.0 - Add iSCSI export support.
Biser Milanovd684c1c2024-11-22 12:12:59 +0200649 """
650
Biser Milanov90b5a142024-11-28 11:33:39 +0200651- VERSION = '2.1.0'
652+ VERSION = '2.2.0'
Biser Milanovd684c1c2024-11-22 12:12:59 +0200653 CI_WIKI_NAME = 'StorPool_distributed_storage_CI'
654
655 def __init__(self, *args, **kwargs):
Biser Milanov90b5a142024-11-28 11:33:39 +0200656@@ -280,6 +339,7 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300657 self._ourIdInt = None
Biser Milanov90b5a142024-11-28 11:33:39 +0200658 self._sp_api = None
659 self._volume_prefix = None
Biser Milanovd684c1c2024-11-22 12:12:59 +0200660+ self._use_iscsi = False
Peter Pentchev9c24be92022-09-26 22:35:24 +0300661
662 @staticmethod
663 def get_driver_options():
Biser Milanov90b5a142024-11-28 11:33:39 +0200664@@ -351,10 +411,327 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300665 raise StorPoolConfigurationInvalid(
666 section=hostname, param='SP_OURID', error=e)
667
668+ def _connector_wants_iscsi(self, connector):
669+ """Should we do this export via iSCSI?
670+
671+ Check the configuration to determine whether this connector is
672+ expected to provide iSCSI exports as opposed to native StorPool
673+ protocol ones. Match the initiator's IQN against the list of
Biser Milanovd684c1c2024-11-22 12:12:59 +0200674+ patterns supplied in the "storpool_iscsi_export_to" configuration
675+ setting.
Peter Pentchev9c24be92022-09-26 22:35:24 +0300676+ """
677+ if connector is None:
678+ return False
679+ if self._use_iscsi:
680+ LOG.debug(' - forcing iSCSI for all exported volumes')
681+ return True
682+ if connector.get('storpool_wants_iscsi'):
683+ LOG.debug(' - forcing iSCSI for the controller')
684+ return True
685+
686+ try:
687+ iqn = connector.get('initiator')
688+ except Exception:
689+ iqn = None
690+ try:
691+ host = connector.get('host')
692+ except Exception:
693+ host = None
694+ if iqn is None or host is None:
695+ LOG.debug(' - this connector certainly does not want iSCSI')
696+ return False
697+
698+ LOG.debug(' - check whether %(host)s (%(iqn)s) wants iSCSI',
699+ {
700+ 'host': host,
701+ 'iqn': iqn,
702+ })
703+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200704+ export_to = self.configuration.storpool_iscsi_export_to
Peter Pentchev9c24be92022-09-26 22:35:24 +0300705+ if export_to is None:
706+ return False
707+
708+ for pat in export_to.split():
709+ LOG.debug(' - matching against %(pat)s', {'pat': pat})
710+ if fnmatch.fnmatch(iqn, pat):
711+ LOG.debug(' - got it!')
712+ return True
713+ LOG.debug(' - nope')
714+ return False
715+
716 def validate_connector(self, connector):
717+ if self._connector_wants_iscsi(connector):
718+ return True
719 return self._storpool_client_id(connector) >= 0
720
721+ def _get_iscsi_config(self, iqn, volume_id):
722+ """Get the StorPool iSCSI config items pertaining to this volume.
723+
724+ Find the elements of the StorPool iSCSI configuration tree that
725+ will be needed to create, ensure, or remove the iSCSI export of
726+ the specified volume to the specified initiator.
727+ """
Biser Milanov90b5a142024-11-28 11:33:39 +0200728+ cfg = self._sp_api.get_iscsi_config()
Peter Pentchev9c24be92022-09-26 22:35:24 +0300729+
Biser Milanovd684c1c2024-11-22 12:12:59 +0200730+ pg_name = self.configuration.storpool_iscsi_portal_group
Peter Pentchev9c24be92022-09-26 22:35:24 +0300731+ pg_found = [
Biser Milanov90b5a142024-11-28 11:33:39 +0200732+ pg for pg in cfg['iscsi']['portalGroups'].values() if pg['name'] == pg_name
Peter Pentchev9c24be92022-09-26 22:35:24 +0300733+ ]
734+ if not pg_found:
735+ raise Exception('StorPool Cinder iSCSI configuration error: '
736+ 'no portal group "{pg}"'.format(pg=pg_name))
737+ pg = pg_found[0]
738+
739+ # Do we know about this initiator?
740+ i_found = [
Biser Milanov90b5a142024-11-28 11:33:39 +0200741+ init for init in cfg['iscsi']['initiators'].values() if init['name'] == iqn
Peter Pentchev9c24be92022-09-26 22:35:24 +0300742+ ]
743+ if i_found:
744+ initiator = i_found[0]
745+ else:
746+ initiator = None
747+
748+ # Is this volume already being exported?
Biser Milanov90b5a142024-11-28 11:33:39 +0200749+ volname = self._os_to_sp_volume_name(volume_id)
Peter Pentchev9c24be92022-09-26 22:35:24 +0300750+ t_found = [
Biser Milanov90b5a142024-11-28 11:33:39 +0200751+ tgt for tgt in cfg['iscsi']['targets'].values() if tgt['volume'] == volname
Peter Pentchev9c24be92022-09-26 22:35:24 +0300752+ ]
753+ if t_found:
754+ target = t_found[0]
755+ else:
756+ target = None
757+
758+ # OK, so is this volume being exported to this initiator?
759+ export = None
760+ if initiator is not None and target is not None:
761+ e_found = [
Biser Milanov90b5a142024-11-28 11:33:39 +0200762+ exp for exp in initiator['exports']
763+ if exp['portalGroup'] == pg['name'] and exp['target'] == target['name']
Peter Pentchev9c24be92022-09-26 22:35:24 +0300764+ ]
765+ if e_found:
766+ export = e_found[0]
767+
768+ return {
769+ 'cfg': cfg,
770+ 'pg': pg,
771+ 'initiator': initiator,
772+ 'target': target,
773+ 'export': export,
774+ 'volume_name': volname,
775+ 'volume_id': volume_id,
776+ }
777+
778+ def _create_iscsi_export(self, volume, connector):
779+ """Create (if needed) an iSCSI export for the StorPool volume."""
780+ LOG.debug(
781+ '_create_iscsi_export() invoked for volume '
782+ '"%(vol_name)s" (%(vol_id)s) connector %(connector)s',
783+ {
784+ 'vol_name': volume['display_name'],
785+ 'vol_id': volume['id'],
786+ 'connector': connector,
787+ }
788+ )
789+ iqn = connector['initiator']
790+ try:
791+ cfg = self._get_iscsi_config(iqn, volume['id'])
792+ except Exception as exc:
793+ LOG.error(
794+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
795+ )
796+ raise
797+
798+ if cfg['initiator'] is None:
Biser Milanovd684c1c2024-11-22 12:12:59 +0200799+ if not (self.configuration.storpool_iscsi_learn_initiator_iqns or
800+ self.configuration.storpool_iscsi_cinder_volume and
Peter Pentchev9c24be92022-09-26 22:35:24 +0300801+ connector.get('storpool_wants_iscsi')):
802+ raise Exception('The "{iqn}" initiator IQN for the "{host}" '
803+ 'host is not defined in the StorPool '
804+ 'configuration.'
805+ .format(iqn=iqn, host=connector['host']))
806+ else:
807+ LOG.info('Creating a StorPool iSCSI initiator '
808+ 'for "{host}s" ({iqn}s)',
809+ {'host': connector['host'], 'iqn': iqn})
Biser Milanov90b5a142024-11-28 11:33:39 +0200810+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300811+ 'commands': [
812+ {
813+ 'createInitiator': {
814+ 'name': iqn,
815+ 'username': '',
816+ 'secret': '',
817+ },
818+ },
819+ {
820+ 'initiatorAddNetwork': {
821+ 'initiator': iqn,
822+ 'net': '0.0.0.0/0',
823+ },
824+ },
825+ ]
826+ })
827+
828+ if cfg['target'] is None:
829+ LOG.info(
830+ 'Creating a StorPool iSCSI target '
831+ 'for the "%(vol_name)s" volume (%(vol_id)s)',
832+ {
833+ 'vol_name': volume['display_name'],
834+ 'vol_id': volume['id'],
835+ }
836+ )
Biser Milanov90b5a142024-11-28 11:33:39 +0200837+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300838+ 'commands': [
839+ {
840+ 'createTarget': {
841+ 'volumeName': cfg['volume_name'],
842+ },
843+ },
844+ ]
845+ })
846+ cfg = self._get_iscsi_config(iqn, volume['id'])
847+
848+ if cfg['export'] is None:
849+ LOG.info('Creating a StorPool iSCSI export '
850+ 'for the "{vol_name}s" volume ({vol_id}s) '
851+ 'to the "{host}s" initiator ({iqn}s) '
852+ 'in the "{pg}s" portal group',
853+ {
854+ 'vol_name': volume['display_name'],
855+ 'vol_id': volume['id'],
856+ 'host': connector['host'],
857+ 'iqn': iqn,
Biser Milanov90b5a142024-11-28 11:33:39 +0200858+ 'pg': cfg['pg']['name']
Peter Pentchev9c24be92022-09-26 22:35:24 +0300859+ })
Biser Milanov90b5a142024-11-28 11:33:39 +0200860+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300861+ 'commands': [
862+ {
863+ 'export': {
864+ 'initiator': iqn,
Biser Milanov90b5a142024-11-28 11:33:39 +0200865+ 'portalGroup': cfg['pg']['name'],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300866+ 'volumeName': cfg['volume_name'],
867+ },
868+ },
869+ ]
870+ })
871+
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200872+ target_portals = [
Biser Milanov90b5a142024-11-28 11:33:39 +0200873+ "{addr}:3260".format(addr=net['address'])
874+ for net in cfg['pg']['networks']
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200875+ ]
Biser Milanov90b5a142024-11-28 11:33:39 +0200876+ target_iqns = [cfg['target']['name']] * len(target_portals)
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200877+ target_luns = [0] * len(target_portals)
878+ if connector.get('multipath', False):
879+ multipath_settings = {
880+ 'target_iqns': target_iqns,
881+ 'target_portals': target_portals,
882+ 'target_luns': target_luns,
883+ }
884+ else:
885+ multipath_settings = {}
886+
Peter Pentchev9c24be92022-09-26 22:35:24 +0300887+ res = {
888+ 'driver_volume_type': 'iscsi',
889+ 'data': {
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200890+ **multipath_settings,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300891+ 'target_discovered': False,
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200892+ 'target_iqn': target_iqns[0],
893+ 'target_portal': target_portals[0],
894+ 'target_lun': target_luns[0],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300895+ 'volume_id': volume['id'],
896+ 'discard': True,
897+ },
898+ }
899+ LOG.debug('returning %(res)s', {'res': res})
900+ return res
901+
902+ def _remove_iscsi_export(self, volume, connector):
903+ """Remove an iSCSI export for the specified StorPool volume."""
904+ LOG.debug(
905+ '_remove_iscsi_export() invoked for volume '
906+ '"%(vol_name)s" (%(vol_id)s) connector %(conn)s',
907+ {
908+ 'vol_name': volume['display_name'],
909+ 'vol_id': volume['id'],
910+ 'conn': connector,
911+ }
912+ )
913+ try:
914+ cfg = self._get_iscsi_config(connector['initiator'], volume['id'])
915+ except Exception as exc:
916+ LOG.error(
917+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
918+ )
919+ raise
920+
921+ if cfg['export'] is not None:
922+ LOG.info('Removing the StorPool iSCSI export '
923+ 'for the "%(vol_name)s" volume (%(vol_id)s) '
924+ 'to the "%(host)s" initiator (%(iqn)s) '
925+ 'in the "%(pg)s" portal group',
926+ {
927+ 'vol_name': volume['display_name'],
928+ 'vol_id': volume['id'],
929+ 'host': connector['host'],
930+ 'iqn': connector['initiator'],
Biser Milanov90b5a142024-11-28 11:33:39 +0200931+ 'pg': cfg['pg']['name'],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300932+ })
933+ try:
Biser Milanov90b5a142024-11-28 11:33:39 +0200934+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300935+ 'commands': [
936+ {
937+ 'exportDelete': {
Biser Milanov90b5a142024-11-28 11:33:39 +0200938+ 'initiator': cfg['initiator']['name'],
939+ 'portalGroup': cfg['pg']['name'],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300940+ 'volumeName': cfg['volume_name'],
941+ },
942+ },
943+ ]
944+ })
Biser Milanov90b5a142024-11-28 11:33:39 +0200945+ except StorPoolAPIError as e:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300946+ if e.name not in ('objectExists', 'objectDoesNotExist'):
947+ raise
948+ LOG.info('Looks like somebody beat us to it')
949+
950+ if cfg['target'] is not None:
951+ last = True
Biser Milanov90b5a142024-11-28 11:33:39 +0200952+ for initiator in cfg['cfg']['iscsi']['initiators'].values():
953+ if initiator['name'] == cfg['initiator']['name']:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300954+ continue
Biser Milanov90b5a142024-11-28 11:33:39 +0200955+ for exp in initiator['exports']:
956+ if exp['target'] == cfg['target']['name']:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300957+ last = False
958+ break
959+ if not last:
960+ break
961+
962+ if last:
963+ LOG.info(
964+ 'Removing the StorPool iSCSI target '
965+ 'for the "{vol_name}s" volume ({vol_id}s)',
966+ {
967+ 'vol_name': volume['display_name'],
968+ 'vol_id': volume['id'],
969+ }
970+ )
971+ try:
Biser Milanov90b5a142024-11-28 11:33:39 +0200972+ self._sp_api.post_iscsi_config({
Peter Pentchev9c24be92022-09-26 22:35:24 +0300973+ 'commands': [
974+ {
975+ 'deleteTarget': {
976+ 'volumeName': cfg['volume_name'],
977+ },
978+ },
979+ ]
980+ })
Biser Milanov90b5a142024-11-28 11:33:39 +0200981+ except StorPoolAPIError as e:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300982+ if e.name not in ('objectDoesNotExist', 'invalidParam'):
983+ raise
984+ LOG.info('Looks like somebody beat us to it')
985+
986 def initialize_connection(self, volume, connector):
987+ if self._connector_wants_iscsi(connector):
988+ return self._create_iscsi_export(volume, connector)
989 return {'driver_volume_type': 'storpool',
990 'data': {
991 'client_id': self._storpool_client_id(connector),
Biser Milanov90b5a142024-11-28 11:33:39 +0200992@@ -363,6 +740,9 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300993 }}
994
995 def terminate_connection(self, volume, connector, **kwargs):
996+ if self._connector_wants_iscsi(connector):
997+ LOG.debug('- removing an iSCSI export')
998+ self._remove_iscsi_export(volume, connector)
999 pass
1000
1001 def create_snapshot(self, snapshot):
Biser Milanov90b5a142024-11-28 11:33:39 +02001002@@ -464,11 +844,20 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001003 )
1004
1005 def create_export(self, context, volume, connector):
1006- pass
1007+ if self._connector_wants_iscsi(connector):
1008+ LOG.debug('- creating an iSCSI export')
1009+ self._create_iscsi_export(volume, connector)
1010
1011 def remove_export(self, context, volume):
1012 pass
1013
1014+ def _attach_volume(self, context, volume, properties, remote=False):
Biser Milanovd684c1c2024-11-22 12:12:59 +02001015+ if self.configuration.storpool_iscsi_cinder_volume and not remote:
Peter Pentchev9c24be92022-09-26 22:35:24 +03001016+ LOG.debug('- adding the "storpool_wants_iscsi" flag')
1017+ properties['storpool_wants_iscsi'] = True
1018+
1019+ return super()._attach_volume(context, volume, properties, remote)
1020+
1021 def delete_volume(self, volume):
Biser Milanov90b5a142024-11-28 11:33:39 +02001022 name = self._os_to_sp_volume_name(volume['id'])
Peter Pentchev9c24be92022-09-26 22:35:24 +03001023 try:
Biser Milanov90b5a142024-11-28 11:33:39 +02001024@@ -502,6 +891,17 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001025 LOG.error("StorPoolDriver API initialization failed: %s", e)
1026 raise
1027
Biser Milanovd684c1c2024-11-22 12:12:59 +02001028+ export_to = self.configuration.storpool_iscsi_export_to
Peter Pentchev9c24be92022-09-26 22:35:24 +03001029+ export_to_set = export_to is not None and export_to.split()
Biser Milanovd684c1c2024-11-22 12:12:59 +02001030+ vol_iscsi = self.configuration.storpool_iscsi_cinder_volume
1031+ pg_name = self.configuration.storpool_iscsi_portal_group
Peter Pentchev9c24be92022-09-26 22:35:24 +03001032+ if (export_to_set or vol_iscsi) and pg_name is None:
Biser Milanovd684c1c2024-11-22 12:12:59 +02001033+ msg = _('The "storpool_iscsi_portal_group" option is required if '
1034+ 'any patterns are listed in "storpool_iscsi_export_to"')
Peter Pentchev9c24be92022-09-26 22:35:24 +03001035+ raise exception.VolumeDriverException(message=msg)
1036+
1037+ self._use_iscsi = export_to == "*"
1038+
1039 def _update_volume_stats(self):
1040 try:
Biser Milanov90b5a142024-11-28 11:33:39 +02001041 dl = self._sp_api.disks_list()
1042@@ -527,7 +927,7 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001043 'total_capacity_gb': total / units.Gi,
1044 'free_capacity_gb': free / units.Gi,
1045 'reserved_percentage': 0,
1046- 'multiattach': True,
Peter Pentchev5a9f8a62023-12-06 10:40:18 +02001047+ 'multiattach': self._use_iscsi,
Peter Pentchev9c24be92022-09-26 22:35:24 +03001048 'QoS_support': False,
1049 'thick_provisioning_support': False,
1050 'thin_provisioning_support': True,
Biser Milanov90b5a142024-11-28 11:33:39 +02001051@@ -546,7 +946,9 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +03001052 'volume_backend_name') or 'storpool',
1053 'vendor_name': 'StorPool',
1054 'driver_version': self.VERSION,
1055- 'storage_protocol': constants.STORPOOL,
1056+ 'storage_protocol': (
1057+ constants.ISCSI if self._use_iscsi else constants.STORPOOL
1058+ ),
Peter Pentchevacaaa382023-02-28 11:26:13 +02001059 # Driver capabilities
Peter Pentchev9c24be92022-09-26 22:35:24 +03001060 'clone_across_pools': True,
1061 'sparse_copy_volume': True,
Peter Pentchevacaaa382023-02-28 11:26:13 +02001062diff --git a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
Biser Milanov90b5a142024-11-28 11:33:39 +02001063index d2c5895a9..1ba0d2862 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +03001064--- a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
1065+++ b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
Peter Pentchevacaaa382023-02-28 11:26:13 +02001066@@ -19,12 +19,15 @@ Prerequisites
Peter Pentchev9c24be92022-09-26 22:35:24 +03001067 * The controller and all the compute nodes must have access to the StorPool
1068 API service.
1069
1070-* All nodes where StorPool-backed volumes will be attached must have access to
1071+* If iSCSI is not being used as a transport (see below), all nodes where
1072+ StorPool-backed volumes will be attached must have access to
1073 the StorPool data network and run the ``storpool_block`` service.
1074
1075-* If StorPool-backed Cinder volumes need to be created directly from Glance
1076- images, then the node running the ``cinder-volume`` service must also have
1077- access to the StorPool data network and run the ``storpool_block`` service.
1078+* If Glance uses Cinder as its image store, or if StorPool-backed Cinder
1079+ volumes need to be created directly from Glance images, and iSCSI is not
1080+ being used as a transport, then the node running the ``cinder-volume``
1081+ service must also have access to the StorPool data network and run
1082+ the ``storpool_block`` service.
1083
1084 * All nodes that need to access the StorPool API (the compute nodes and
1085 the node running the ``cinder-volume`` service) must have the following
Biser Milanov90b5a142024-11-28 11:33:39 +02001086@@ -34,6 +37,33 @@ Prerequisites
Peter Pentchev9c24be92022-09-26 22:35:24 +03001087 * the storpool Python bindings package
1088 * the storpool.spopenstack Python helper package
1089
1090+Using iSCSI as the transport protocol
1091+-------------------------------------
1092+
1093+The StorPool distributed storage system uses its own, highly optimized and
1094+tailored for its specifics, network protocol for communication between
1095+the storage servers and the clients (the OpenStack cluster nodes where
1096+StorPool-backed volumes will be attached). There are cases when granting
1097+various nodes access to the StorPool data network or installing and
1098+running the ``storpool_block`` client service on them may pose difficulties.
1099+The StorPool servers may also expose the user-created volumes and snapshots
1100+using the standard iSCSI protocol that only requires TCP routing and
1101+connectivity between the storage servers and the StorPool clients.
1102+The StorPool Cinder driver may be configured to export volumes and
Biser Milanov90b5a142024-11-28 11:33:39 +02001103+snapshots via iSCSI using the ``iscsi_export_to`` and ``iscsi_portal_group``
1104+configuration options.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001105+
1106+Additionally, even if e.g. the hypervisor nodes running Nova will use
1107+the StorPool network protocol and run the ``storpool_block`` service
Biser Milanov90b5a142024-11-28 11:33:39 +02001108+(so the ``iscsi_export_to`` option has its default empty string value),
1109+the ``iscsi_cinder_volume`` option configures the StorPool Cinder driver
1110+so that only the ``cinder-volume`` service will use the iSCSI protocol when
1111+attaching volumes and snapshots to transfer data to and from Glance images.
Biser Milanovd684c1c2024-11-22 12:12:59 +02001112+
1113+Multiattach support for StorPool is only enabled if iSCSI is used:
Biser Milanov90b5a142024-11-28 11:33:39 +02001114+``iscsi_export_to`` is set to ``*``, that is, when all StorPool volumes
1115+will be exported via iSCSI to all initiators.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001116+
1117 Configuring the StorPool volume driver
1118 --------------------------------------
1119
Biser Milanov90b5a142024-11-28 11:33:39 +02001120@@ -55,6 +85,32 @@ volume backend definition) and per volume type:
Peter Pentchev9c24be92022-09-26 22:35:24 +03001121 with the default placement constraints for the StorPool cluster.
1122 The default value for the chain replication is 3.
1123
Peter Pentchevea354462023-07-18 11:15:56 +03001124+In addition, if the iSCSI protocol is used to access the StorPool cluster as
1125+described in the previous section, the following options may be defined in
1126+the ``cinder.conf`` volume backend definition:
1127+
Biser Milanov90b5a142024-11-28 11:33:39 +02001128+- ``iscsi_export_to``: if set to the value ``*``, the StorPool Cinder driver
1129+ will export volumes and snapshots using the iSCSI protocol instead of
1130+ the StorPool network protocol. The ``iscsi_portal_group`` option must also
1131+ be specified.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001132+
Biser Milanov90b5a142024-11-28 11:33:39 +02001133+- ``iscsi_portal_group``: if the ``iscsi_export_to`` option is set to
1134+ the value ``*`` or the ``iscsi_cinder_volume`` option is turned on,
1135+ this option specifies the name of the iSCSI portal group that Cinder
1136+ volumes will be exported to.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001137+
Biser Milanov90b5a142024-11-28 11:33:39 +02001138+- ``iscsi_cinder_volume``: if enabled, even if the ``iscsi_export_to`` option
1139+ has its default empty value, the ``cinder-volume`` service will use iSCSI
1140+ to attach the volumes and snapshots for transferring data to and from
1141+ Glance images if Glance is configured to use the Cinder glance_store.
Peter Pentchev9c24be92022-09-26 22:35:24 +03001142+
Biser Milanov90b5a142024-11-28 11:33:39 +02001143+- ``iscsi_learn_initiator_iqns``: if enabled, the StorPool Cinder driver will
1144+ automatically use the StorPool API to create definitions for new initiators
1145+ in the StorPool cluster's configuration. This is the default behavior of
1146+ the driver; it may be disabled in the rare case if, e.g. because of site
1147+ policy, OpenStack iSCSI initiators (e.g. Nova hypervisors) need to be
1148+ explicitly allowed to use the StorPool iSCSI targets.
Peter Pentchevea354462023-07-18 11:15:56 +03001149+
Peter Pentchev9c24be92022-09-26 22:35:24 +03001150 Using the StorPool volume driver
1151 --------------------------------
1152
Peter Pentchevacaaa382023-02-28 11:26:13 +02001153--
Biser Milanovd684c1c2024-11-22 12:12:59 +020011542.43.0
Peter Pentchevacaaa382023-02-28 11:26:13 +02001155