blob: 8c07013287c3eb612f85506aa8ea3295468f6af7 [file] [log] [blame]
Peter Pentchev5a9f8a62023-12-06 10:40:18 +02001From 15fe701cfc698a2d8d3ce12ba33983a42e50411e Mon Sep 17 00:00:00 2001
Peter Pentchev9c24be92022-09-26 22:35:24 +03002From: Peter Penchev <openstack-dev@storpool.com>
3Date: Mon, 12 Mar 2018 12:00:10 +0200
Peter Pentchevacaaa382023-02-28 11:26:13 +02004Subject: [PATCH 10/10] Add iSCSI export support to the StorPool driver
Peter Pentchev9c24be92022-09-26 22:35:24 +03005
6Add four new driver options:
7- iscsi_cinder_volume: use StorPool iSCSI attachments whenever
8 the cinder-volume service needs to attach a volume to the controller,
9 e.g. for copying an image to a volume or vice versa
10- iscsi_export_to:
11 - an empty string to use the StorPool native protocol for exporting volumes
12 protocol for exporting volumes)
13 - the string "*" to always use iSCSI for exporting volumes
14 - an experimental, not fully supported list of IQN patterns to export
15 volumes to using iSCSI; this results in a Cinder driver that exports
16 different volumes using different storage protocols
17- iscsi_portal_group: the name of the iSCSI portal group defined in
18 the StorPool configuration to use for these export
19- iscsi_learn_initiator_iqns: automatically create StorPool configuration
20 records for an initiator when a volume is first exported to it
21
22When exporting volumes via iSCSI, report the storage protocol as "iSCSI" and
23disable multiattach (the StorPool CI failures with iSCSI multiattach may need
24further investigation).
25
26Change-Id: I9de64306e0e6976268df782053b0651dd1cca96f
27---
Peter Pentchev5a9f8a62023-12-06 10:40:18 +020028 .../unit/volume/drivers/test_storpool.py | 437 +++++++++++++++++-
Peter Pentchevea354462023-07-18 11:15:56 +030029 cinder/volume/drivers/storpool.py | 374 ++++++++++++++-
30 .../drivers/storpool-volume-driver.rst | 60 ++-
Peter Pentchev5a9f8a62023-12-06 10:40:18 +020031 .../storpool-iscsi-cefcfe590a07c5c7.yaml | 13 +
32 4 files changed, 874 insertions(+), 10 deletions(-)
Peter Pentchevea354462023-07-18 11:15:56 +030033 create mode 100644 releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
Peter Pentchev9c24be92022-09-26 22:35:24 +030034
Peter Pentchevacaaa382023-02-28 11:26:13 +020035diff --git a/cinder/tests/unit/volume/drivers/test_storpool.py b/cinder/tests/unit/volume/drivers/test_storpool.py
Peter Pentchev5a9f8a62023-12-06 10:40:18 +020036index 95a1ffffd..2b76e7c75 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
Peter Pentchevea354462023-07-18 11:15:56 +030039@@ -14,15 +14,25 @@
40 # under the License.
41
42
43+from __future__ import annotations
44+
45+import dataclasses
46 import itertools
47 import re
48 import sys
49+from typing import Any, NamedTuple, TYPE_CHECKING # noqa: H301
50 from unittest import mock
51
52 import ddt
53 from oslo_utils import units
54 import six
55
56+if TYPE_CHECKING:
57+ if sys.version_info >= (3, 11):
58+ from typing import Self
59+ else:
60+ from typing_extensions import Self
61+
62
63 fakeStorPool = mock.Mock()
64 fakeStorPool.spopenstack = mock.Mock()
65@@ -32,12 +42,21 @@ fakeStorPool.sptypes = mock.Mock()
Peter Pentchev9c24be92022-09-26 22:35:24 +030066 sys.modules['storpool'] = fakeStorPool
67
68
69+from cinder.common import constants
70 from cinder import exception
Peter Pentchevea354462023-07-18 11:15:56 +030071+from cinder.tests.unit import fake_constants as fconst
Peter Pentchev9c24be92022-09-26 22:35:24 +030072 from cinder.tests.unit import test
73 from cinder.volume import configuration as conf
Peter Pentchevea354462023-07-18 11:15:56 +030074 from cinder.volume.drivers import storpool as driver
75
76
77+_ISCSI_IQN_OURS = 'beleriand'
78+_ISCSI_IQN_OTHER = 'rohan'
79+_ISCSI_IQN_THIRD = 'gondor'
80+_ISCSI_PAT_OTHER = 'roh*'
81+_ISCSI_PAT_BOTH = '*riand roh*'
82+_ISCSI_PORTAL_GROUP = 'openstack_pg'
83+
84 volume_types = {
85 1: {},
86 2: {'storpool_template': 'ssd'},
87@@ -71,6 +90,10 @@ def snapshotName(vtype, vid):
88 return 'os--snap--{t}--{id}'.format(t=vtype, id=vid)
89
90
91+def targetName(vid):
92+ return 'iqn.2012-11.storpool:{id}'.format(id=vid)
93+
94+
95 class MockDisk(object):
96 def __init__(self, diskId):
97 self.id = diskId
98@@ -181,6 +204,273 @@ def MockVolumeUpdateDesc(size):
99 return {'size': size}
100
101
102+@dataclasses.dataclass(frozen=True)
103+class MockIscsiNetwork:
104+ """Mock a StorPool IP CIDR network definition (partially)."""
105+
106+ address: str
107+
108+
109+@dataclasses.dataclass(frozen=True)
110+class MockIscsiPortalGroup:
111+ """Mock a StorPool iSCSI portal group definition (partially)."""
112+
113+ name: str
114+ networks: list[MockIscsiNetwork]
115+
116+
117+@dataclasses.dataclass(frozen=True)
118+class MockIscsiExport:
119+ """Mock a StorPool iSCSI exported volume/target definition."""
120+
121+ portalGroup: str
122+ target: str
123+
124+
125+@dataclasses.dataclass(frozen=True)
126+class MockIscsiInitiator:
127+ """Mock a StorPool iSCSI initiator definition."""
128+
129+ name: str
130+ exports: list[MockIscsiExport]
131+
132+
133+@dataclasses.dataclass(frozen=True)
134+class MockIscsiTarget:
135+ """Mock a StorPool iSCSI volume-to-target mapping definition."""
136+
137+ name: str
138+ volume: str
139+
140+
141+class IscsiTestCase(NamedTuple):
142+ """A single test case for the iSCSI config and export methods."""
143+
144+ initiator: str | None
145+ volume: str | None
146+ exported: bool
147+ commands_count: int
148+
149+
150+@dataclasses.dataclass(frozen=True)
151+class MockIscsiConfig:
152+ """Mock the structure returned by the "get current config" query."""
153+
154+ portalGroups: dict[str, MockIscsiPortalGroup]
155+ initiators: dict[str, MockIscsiInitiator]
156+ targets: dict[str, MockIscsiTarget]
157+
158+ @classmethod
159+ def build(cls, tcase: IscsiTestCase) -> Self:
160+ """Build a test config structure."""
161+ initiators = {
162+ '0': MockIscsiInitiator(name=_ISCSI_IQN_OTHER, exports=[]),
163+ }
164+ if tcase.initiator is not None:
165+ initiators['1'] = MockIscsiInitiator(
166+ name=tcase.initiator,
167+ exports=(
168+ [
169+ MockIscsiExport(
170+ portalGroup=_ISCSI_PORTAL_GROUP,
171+ target=targetName(tcase.volume),
172+ ),
173+ ]
174+ if tcase.exported
175+ else []
176+ ),
177+ )
178+
179+ targets = {
180+ '0': MockIscsiTarget(
181+ name=targetName(fconst.VOLUME2_ID),
182+ volume=volumeName(fconst.VOLUME2_ID),
183+ ),
184+ }
185+ if tcase.volume is not None:
186+ targets['1'] = MockIscsiTarget(
187+ name=targetName(tcase.volume),
188+ volume=volumeName(tcase.volume),
189+ )
190+
191+ return cls(
192+ portalGroups={
193+ '0': MockIscsiPortalGroup(
194+ name=_ISCSI_PORTAL_GROUP + '-not',
195+ networks=[],
196+ ),
197+ '1': MockIscsiPortalGroup(
198+ name=_ISCSI_PORTAL_GROUP,
199+ networks=[
200+ MockIscsiNetwork(address="192.0.2.0"),
201+ MockIscsiNetwork(address="195.51.100.0"),
202+ ],
203+ ),
204+ },
205+ initiators=initiators,
206+ targets=targets,
207+ )
208+
209+
210+@dataclasses.dataclass(frozen=True)
211+class MockIscsiConfigTop:
212+ """Mock the top level of the "get the iSCSI configuration" response."""
213+
214+ iscsi: MockIscsiConfig
215+
216+
217+class MockIscsiAPI:
218+ """Mock only the iSCSI-related calls of the StorPool API bindings."""
219+
220+ _asrt: test.TestCase
221+ _configs: list[MockIscsiConfig]
222+
223+ def __init__(
224+ self,
225+ configs: list[MockIscsiConfig],
226+ asrt: test.TestCase,
227+ ) -> None:
228+ """Store the reference to the list of iSCSI config objects."""
229+ self._asrt = asrt
230+ self._configs = configs
231+
232+ def iSCSIConfig(self) -> MockIscsiConfigTop:
233+ """Return the last version of the iSCSI configuration."""
234+ return MockIscsiConfigTop(iscsi=self._configs[-1])
235+
236+ def _handle_export(
237+ self,
238+ cfg: MockIscsiConfig, cmd: dict[str, Any],
239+ ) -> MockIscsiConfig:
240+ """Add an export for an initiator."""
241+ self._asrt.assertDictEqual(
242+ cmd,
243+ {
244+ 'initiator': _ISCSI_IQN_OURS,
245+ 'portalGroup': _ISCSI_PORTAL_GROUP,
246+ 'volumeName': volumeName(fconst.VOLUME_ID),
247+ },
248+ )
249+ self._asrt.assertEqual(cfg.initiators['1'].name, cmd['initiator'])
250+ self._asrt.assertListEqual(cfg.initiators['1'].exports, [])
251+
252+ return dataclasses.replace(
253+ cfg,
254+ initiators={
255+ **cfg.initiators,
256+ '1': dataclasses.replace(
257+ cfg.initiators['1'],
258+ exports=[
259+ MockIscsiExport(
260+ portalGroup=cmd['portalGroup'],
261+ target=targetName(fconst.VOLUME_ID),
262+ ),
263+ ],
264+ ),
265+ },
266+ )
267+
268+ def _handle_create_initiator(
269+ self,
270+ cfg: MockIscsiConfig,
271+ cmd: dict[str, Any],
272+ ) -> MockIscsiConfig:
273+ """Add a whole new initiator."""
274+ self._asrt.assertDictEqual(
275+ cmd,
276+ {
277+ 'name': _ISCSI_IQN_OURS,
278+ 'username': '',
279+ 'secret': '',
280+ },
281+ )
282+ self._asrt.assertNotIn(
283+ cmd['name'],
284+ [init.name for init in cfg.initiators.values()],
285+ )
286+ self._asrt.assertListEqual(sorted(cfg.initiators), ['0'])
287+
288+ return dataclasses.replace(
289+ cfg,
290+ initiators={
291+ **cfg.initiators,
292+ '1': MockIscsiInitiator(name=cmd['name'], exports=[]),
293+ },
294+ )
295+
296+ def _handle_create_target(
297+ self,
298+ cfg: MockIscsiConfig,
299+ cmd: dict[str, Any],
300+ ) -> MockIscsiConfig:
301+ """Add a target for a volume so that it may be exported."""
302+ self._asrt.assertDictEqual(
303+ cmd,
304+ {'volumeName': volumeName(fconst.VOLUME_ID)},
305+ )
306+ self._asrt.assertListEqual(sorted(cfg.targets), ['0'])
307+ return dataclasses.replace(
308+ cfg,
309+ targets={
310+ **cfg.targets,
311+ '1': MockIscsiTarget(
312+ name=targetName(fconst.VOLUME_ID),
313+ volume=volumeName(fconst.VOLUME_ID),
314+ ),
315+ },
316+ )
317+
318+ def _handle_initiator_add_network(
319+ self,
320+ cfg: MockIscsiConfig,
321+ cmd: dict[str, Any],
322+ ) -> MockIscsiConfig:
323+ """Add a network that an initiator is allowed to log in from."""
324+ self._asrt.assertDictEqual(
325+ cmd,
326+ {
327+ 'initiator': _ISCSI_IQN_OURS,
328+ 'net': '0.0.0.0/0',
329+ },
330+ )
331+ return dataclasses.replace(cfg)
332+
333+ _CMD_HANDLERS = {
334+ 'createInitiator': _handle_create_initiator,
335+ 'createTarget': _handle_create_target,
336+ 'export': _handle_export,
337+ 'initiatorAddNetwork': _handle_initiator_add_network,
338+ }
339+
340+ def iSCSIConfigChange(
341+ self,
342+ commands: dict[str, list[dict[str, dict[str, Any]]]],
343+ ) -> None:
344+ """Apply the requested changes to the iSCSI configuration.
345+
346+ This method adds a new config object to the configs list,
347+ making a shallow copy of the last one and applying the changes
348+ specified in the list of commands.
349+ """
350+ self._asrt.assertListEqual(sorted(commands), ['commands'])
351+ self._asrt.assertGreater(len(commands['commands']), 0)
352+ for cmd in commands['commands']:
353+ keys = sorted(cmd.keys())
354+ cmd_name = keys[0]
355+ self._asrt.assertListEqual(keys, [cmd_name])
356+ handler = self._CMD_HANDLERS[cmd_name]
357+ new_cfg = handler(self, self._configs[-1], cmd[cmd_name])
358+ self._configs.append(new_cfg)
359+
360+
361+_ISCSI_TEST_CASES = [
362+ IscsiTestCase(None, None, False, 4),
363+ IscsiTestCase(_ISCSI_IQN_OURS, None, False, 2),
364+ IscsiTestCase(_ISCSI_IQN_OURS, fconst.VOLUME_ID, False, 1),
365+ IscsiTestCase(_ISCSI_IQN_OURS, fconst.VOLUME_ID, True, 0),
366+]
367+
368+
369 def MockSPConfig(section = 's01'):
370 res = {}
371 m = re.match('^s0*([A-Za-z0-9]+)$', section)
372@@ -222,7 +512,15 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300373 self.cfg.volume_backend_name = 'storpool_test'
374 self.cfg.storpool_template = None
375 self.cfg.storpool_replication = 3
376+ self.cfg.iscsi_cinder_volume = False
377+ self.cfg.iscsi_export_to = ''
Peter Pentchevea354462023-07-18 11:15:56 +0300378+ self.cfg.iscsi_learn_initiator_iqns = True
379+ self.cfg.iscsi_portal_group = _ISCSI_PORTAL_GROUP
Peter Pentchev9c24be92022-09-26 22:35:24 +0300380+
Peter Pentchevea354462023-07-18 11:15:56 +0300381+ self._setup_test_driver()
382
Peter Pentchev9c24be92022-09-26 22:35:24 +0300383+ def _setup_test_driver(self):
384+ """Initialize a StorPool driver as per the current configuration."""
385 mock_exec = mock.Mock()
386 mock_exec.return_value = ('', '')
387
Peter Pentchevea354462023-07-18 11:15:56 +0300388@@ -231,7 +529,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300389 self.driver.check_for_setup_error()
390
391 @ddt.data(
392- (5, TypeError),
393+ (5, (TypeError, AttributeError)),
394 ({'no-host': None}, KeyError),
395 ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
396 ({'host': 's01'}, None),
Peter Pentchevea354462023-07-18 11:15:56 +0300397@@ -247,7 +545,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300398 conn)
399
400 @ddt.data(
401- (5, TypeError),
402+ (5, (TypeError, AttributeError)),
403 ({'no-host': None}, KeyError),
404 ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
405 )
Peter Pentchev5a9f8a62023-12-06 10:40:18 +0200406@@ -286,7 +584,7 @@ class StorPoolTestCase(test.TestCase):
407 self.assertEqual(21, pool['total_capacity_gb'])
408 self.assertEqual(5, int(pool['free_capacity_gb']))
409
410- self.assertTrue(pool['multiattach'])
411+ self.assertFalse(pool['multiattach'])
412 self.assertFalse(pool['QoS_support'])
413 self.assertFalse(pool['thick_provisioning_support'])
414 self.assertTrue(pool['thin_provisioning_support'])
Peter Pentchevea354462023-07-18 11:15:56 +0300415@@ -644,3 +942,136 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300416 self.driver.get_pool({
417 'volume_type': volume_type
418 }))
419+
420+ @ddt.data(
421+ # The default values
Peter Pentchevea354462023-07-18 11:15:56 +0300422+ ('', False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300423+
424+ # Export to all
Peter Pentchevea354462023-07-18 11:15:56 +0300425+ ('*', True, constants.ISCSI, _ISCSI_IQN_OURS, True),
426+ ('*', True, constants.ISCSI, _ISCSI_IQN_OURS, True),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300427+
428+ # Only export to the controller
Peter Pentchevea354462023-07-18 11:15:56 +0300429+ ('', False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300430+
431+ # Some of the not-fully-supported pattern lists
Peter Pentchevea354462023-07-18 11:15:56 +0300432+ (_ISCSI_PAT_OTHER, False, constants.STORPOOL, _ISCSI_IQN_OURS, False),
433+ (_ISCSI_PAT_OTHER, False, constants.STORPOOL, _ISCSI_IQN_OTHER, True),
434+ (_ISCSI_PAT_BOTH, False, constants.STORPOOL, _ISCSI_IQN_OURS, True),
435+ (_ISCSI_PAT_BOTH, False, constants.STORPOOL, _ISCSI_IQN_OTHER, True),
Peter Pentchev9c24be92022-09-26 22:35:24 +0300436+ )
437+ @ddt.unpack
438+ def test_wants_iscsi(self, iscsi_export_to, use_iscsi, storage_protocol,
439+ hostname, expected):
440+ """Check the "should this export use iSCSI?" detection."""
441+ self.cfg.iscsi_export_to = iscsi_export_to
442+ self._setup_test_driver()
443+ self.assertEqual(self.driver._use_iscsi, use_iscsi)
444+
445+ # Make sure the driver reports the correct protocol in the stats
446+ self.driver._update_volume_stats()
447+ self.assertEqual(self.driver._stats["vendor_name"], "StorPool")
448+ self.assertEqual(self.driver._stats["storage_protocol"],
449+ storage_protocol)
450+
451+ def check(conn, forced, expected):
452+ """Pass partially or completely valid connector info."""
453+ for initiator in (None, hostname):
Peter Pentchevea354462023-07-18 11:15:56 +0300454+ for host in (None, _ISCSI_IQN_THIRD):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300455+ self.assertEqual(
456+ self.driver._connector_wants_iscsi({
457+ "host": host,
458+ "initiator": initiator,
459+ **conn,
460+ }),
461+ expected if initiator is not None and host is not None
462+ else forced)
463+
464+ # If iscsi_cinder_volume is set and this is the controller, then yes.
465+ check({"storpool_wants_iscsi": True}, True, True)
466+
467+ # If iscsi_cinder_volume is not set or this is not the controller, then
468+ # look at the specified expected value.
469+ check({"storpool_wants_iscsi": False}, use_iscsi, expected)
470+ check({}, use_iscsi, expected)
Peter Pentchevea354462023-07-18 11:15:56 +0300471+
472+ def _validate_iscsi_config(
473+ self,
474+ cfg: MockIscsiConfig,
475+ res: dict[str, Any],
476+ tcase: IscsiTestCase,
477+ ) -> None:
478+ """Make sure the returned structure makes sense."""
479+ initiator = res['initiator']
480+ cfg_initiator = cfg.initiators.get('1')
481+
482+ self.assertIs(res['cfg'].iscsi, cfg)
483+ self.assertEqual(res['pg'].name, _ISCSI_PORTAL_GROUP)
484+
485+ if tcase.initiator is None:
486+ self.assertIsNone(initiator)
487+ else:
488+ self.assertIsNotNone(initiator)
489+ self.assertEqual(initiator, cfg_initiator)
490+
491+ if tcase.volume is None:
492+ self.assertIsNone(res['target'])
493+ else:
494+ self.assertIsNotNone(res['target'])
495+ self.assertEqual(res['target'], cfg.targets.get('1'))
496+
497+ if tcase.initiator is None:
498+ self.assertIsNone(cfg_initiator)
499+ self.assertIsNone(res['export'])
500+ else:
501+ self.assertIsNotNone(cfg_initiator)
502+ if tcase.exported:
503+ self.assertIsNotNone(res['export'])
504+ self.assertEqual(res['export'], cfg_initiator.exports[0])
505+ else:
506+ self.assertIsNone(res['export'])
507+
508+ @ddt.data(*_ISCSI_TEST_CASES)
509+ def test_iscsi_get_config(self, tcase: IscsiTestCase) -> None:
510+ """Make sure the StorPool iSCSI configuration is parsed correctly."""
511+ cfg_orig = MockIscsiConfig.build(tcase)
512+ configs = [cfg_orig]
513+ iapi = MockIscsiAPI(configs, self)
514+ with mock.patch.object(self.driver._attach, 'api', new=lambda: iapi):
515+ res = self.driver._get_iscsi_config(
516+ _ISCSI_IQN_OURS,
517+ fconst.VOLUME_ID,
518+ )
519+
520+ self._validate_iscsi_config(cfg_orig, res, tcase)
521+
522+ @ddt.data(*_ISCSI_TEST_CASES)
523+ def test_iscsi_create_export(self, tcase: IscsiTestCase) -> None:
524+ """Make sure _create_iscsi_export() makes the right API calls."""
525+ cfg_orig = MockIscsiConfig.build(tcase)
526+ configs = [cfg_orig]
527+ iapi = MockIscsiAPI(configs, self)
528+ with mock.patch.object(self.driver._attach, 'api', new=lambda: iapi):
529+ self.driver._create_iscsi_export(
530+ {
531+ 'id': fconst.VOLUME_ID,
532+ 'display_name': fconst.VOLUME_NAME,
533+ },
534+ {
535+ # Yeah, okay, so we cheat a little bit here...
536+ 'host': _ISCSI_IQN_OURS + '.hostname',
537+ 'initiator': _ISCSI_IQN_OURS,
538+ },
539+ )
540+
541+ self.assertEqual(len(configs), tcase.commands_count + 1)
542+ cfg_final = configs[-1]
543+ self.assertEqual(cfg_final.initiators['1'].name, _ISCSI_IQN_OURS)
544+ self.assertEqual(
545+ cfg_final.initiators['1'].exports[0].target,
546+ targetName(fconst.VOLUME_ID),
547+ )
548+ self.assertEqual(
549+ cfg_final.targets['1'].volume,
550+ volumeName(fconst.VOLUME_ID),
551+ )
Peter Pentchevacaaa382023-02-28 11:26:13 +0200552diff --git a/cinder/volume/drivers/storpool.py b/cinder/volume/drivers/storpool.py
Peter Pentchev5a9f8a62023-12-06 10:40:18 +0200553index b15a201c3..d8c420a66 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +0300554--- a/cinder/volume/drivers/storpool.py
555+++ b/cinder/volume/drivers/storpool.py
556@@ -15,6 +15,7 @@
557
558 """StorPool block device driver"""
559
560+import fnmatch
561 import platform
562
563 from oslo_config import cfg
Peter Pentchevacaaa382023-02-28 11:26:13 +0200564@@ -44,6 +45,31 @@ if storpool:
Peter Pentchev9c24be92022-09-26 22:35:24 +0300565
566
567 storpool_opts = [
568+ cfg.BoolOpt('iscsi_cinder_volume',
569+ default=False,
570+ help='Let the cinder-volume service use iSCSI instead of '
571+ 'the StorPool block device driver for accessing '
572+ 'StorPool volumes, e.g. when creating a volume from '
573+ 'an image or vice versa.'),
574+ cfg.StrOpt('iscsi_export_to',
575+ default='',
576+ help='Whether to export volumes using iSCSI. '
577+ 'An empty string (the default) makes the driver export '
578+ 'all volumes using the StorPool native network protocol. '
579+ 'The value "*" makes the driver export all volumes using '
580+ 'iSCSI. '
581+ 'Any other value leads to an experimental not fully '
582+ 'supported configuration and is interpreted as '
583+ 'a whitespace-separated list of patterns for IQNs for '
584+ 'hosts that need volumes to be exported via iSCSI, e.g. '
585+ '"iqn.1991-05.com.microsoft:\\*" for Windows hosts.'),
586+ cfg.BoolOpt('iscsi_learn_initiator_iqns',
587+ default=True,
588+ help='Create a StorPool record for a new initiator as soon as '
589+ 'Cinder asks for a volume to be exported to it.'),
590+ cfg.StrOpt('iscsi_portal_group',
591+ default=None,
592+ help='The portal group to export volumes via iSCSI in.'),
593 cfg.StrOpt('storpool_template',
594 default=None,
595 help='The StorPool template for volumes with no type.'),
Peter Pentchevacaaa382023-02-28 11:26:13 +0200596@@ -105,6 +131,7 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300597 self._ourId = None
598 self._ourIdInt = None
599 self._attach = None
600+ self._use_iscsi = None
601
602 @staticmethod
603 def get_driver_options():
Peter Pentchevacaaa382023-02-28 11:26:13 +0200604@@ -162,10 +189,326 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300605 raise StorPoolConfigurationInvalid(
606 section=hostname, param='SP_OURID', error=e)
607
608+ def _connector_wants_iscsi(self, connector):
609+ """Should we do this export via iSCSI?
610+
611+ Check the configuration to determine whether this connector is
612+ expected to provide iSCSI exports as opposed to native StorPool
613+ protocol ones. Match the initiator's IQN against the list of
614+ patterns supplied in the "iscsi_export_to" configuration setting.
615+ """
616+ if connector is None:
617+ return False
618+ if self._use_iscsi:
619+ LOG.debug(' - forcing iSCSI for all exported volumes')
620+ return True
621+ if connector.get('storpool_wants_iscsi'):
622+ LOG.debug(' - forcing iSCSI for the controller')
623+ return True
624+
625+ try:
626+ iqn = connector.get('initiator')
627+ except Exception:
628+ iqn = None
629+ try:
630+ host = connector.get('host')
631+ except Exception:
632+ host = None
633+ if iqn is None or host is None:
634+ LOG.debug(' - this connector certainly does not want iSCSI')
635+ return False
636+
637+ LOG.debug(' - check whether %(host)s (%(iqn)s) wants iSCSI',
638+ {
639+ 'host': host,
640+ 'iqn': iqn,
641+ })
642+
643+ export_to = self.configuration.iscsi_export_to
644+ if export_to is None:
645+ return False
646+
647+ for pat in export_to.split():
648+ LOG.debug(' - matching against %(pat)s', {'pat': pat})
649+ if fnmatch.fnmatch(iqn, pat):
650+ LOG.debug(' - got it!')
651+ return True
652+ LOG.debug(' - nope')
653+ return False
654+
655 def validate_connector(self, connector):
656+ if self._connector_wants_iscsi(connector):
657+ return True
658 return self._storpool_client_id(connector) >= 0
659
660+ def _get_iscsi_config(self, iqn, volume_id):
661+ """Get the StorPool iSCSI config items pertaining to this volume.
662+
663+ Find the elements of the StorPool iSCSI configuration tree that
664+ will be needed to create, ensure, or remove the iSCSI export of
665+ the specified volume to the specified initiator.
666+ """
667+ cfg = self._attach.api().iSCSIConfig()
668+
669+ pg_name = self.configuration.iscsi_portal_group
670+ pg_found = [
671+ pg for pg in cfg.iscsi.portalGroups.values() if pg.name == pg_name
672+ ]
673+ if not pg_found:
674+ raise Exception('StorPool Cinder iSCSI configuration error: '
675+ 'no portal group "{pg}"'.format(pg=pg_name))
676+ pg = pg_found[0]
677+
678+ # Do we know about this initiator?
679+ i_found = [
680+ init for init in cfg.iscsi.initiators.values() if init.name == iqn
681+ ]
682+ if i_found:
683+ initiator = i_found[0]
684+ else:
685+ initiator = None
686+
687+ # Is this volume already being exported?
688+ volname = self._attach.volumeName(volume_id)
689+ t_found = [
690+ tgt for tgt in cfg.iscsi.targets.values() if tgt.volume == volname
691+ ]
692+ if t_found:
693+ target = t_found[0]
694+ else:
695+ target = None
696+
697+ # OK, so is this volume being exported to this initiator?
698+ export = None
699+ if initiator is not None and target is not None:
700+ e_found = [
701+ exp for exp in initiator.exports
702+ if exp.portalGroup == pg.name and exp.target == target.name
703+ ]
704+ if e_found:
705+ export = e_found[0]
706+
707+ return {
708+ 'cfg': cfg,
709+ 'pg': pg,
710+ 'initiator': initiator,
711+ 'target': target,
712+ 'export': export,
713+ 'volume_name': volname,
714+ 'volume_id': volume_id,
715+ }
716+
717+ def _create_iscsi_export(self, volume, connector):
718+ """Create (if needed) an iSCSI export for the StorPool volume."""
719+ LOG.debug(
720+ '_create_iscsi_export() invoked for volume '
721+ '"%(vol_name)s" (%(vol_id)s) connector %(connector)s',
722+ {
723+ 'vol_name': volume['display_name'],
724+ 'vol_id': volume['id'],
725+ 'connector': connector,
726+ }
727+ )
728+ iqn = connector['initiator']
729+ try:
730+ cfg = self._get_iscsi_config(iqn, volume['id'])
731+ except Exception as exc:
732+ LOG.error(
733+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
734+ )
735+ raise
736+
737+ if cfg['initiator'] is None:
738+ if not (self.configuration.iscsi_learn_initiator_iqns or
739+ self.configuration.iscsi_cinder_volume and
740+ connector.get('storpool_wants_iscsi')):
741+ raise Exception('The "{iqn}" initiator IQN for the "{host}" '
742+ 'host is not defined in the StorPool '
743+ 'configuration.'
744+ .format(iqn=iqn, host=connector['host']))
745+ else:
746+ LOG.info('Creating a StorPool iSCSI initiator '
747+ 'for "{host}s" ({iqn}s)',
748+ {'host': connector['host'], 'iqn': iqn})
749+ self._attach.api().iSCSIConfigChange({
750+ 'commands': [
751+ {
752+ 'createInitiator': {
753+ 'name': iqn,
754+ 'username': '',
755+ 'secret': '',
756+ },
757+ },
758+ {
759+ 'initiatorAddNetwork': {
760+ 'initiator': iqn,
761+ 'net': '0.0.0.0/0',
762+ },
763+ },
764+ ]
765+ })
766+
767+ if cfg['target'] is None:
768+ LOG.info(
769+ 'Creating a StorPool iSCSI target '
770+ 'for the "%(vol_name)s" volume (%(vol_id)s)',
771+ {
772+ 'vol_name': volume['display_name'],
773+ 'vol_id': volume['id'],
774+ }
775+ )
776+ self._attach.api().iSCSIConfigChange({
777+ 'commands': [
778+ {
779+ 'createTarget': {
780+ 'volumeName': cfg['volume_name'],
781+ },
782+ },
783+ ]
784+ })
785+ cfg = self._get_iscsi_config(iqn, volume['id'])
786+
787+ if cfg['export'] is None:
788+ LOG.info('Creating a StorPool iSCSI export '
789+ 'for the "{vol_name}s" volume ({vol_id}s) '
790+ 'to the "{host}s" initiator ({iqn}s) '
791+ 'in the "{pg}s" portal group',
792+ {
793+ 'vol_name': volume['display_name'],
794+ 'vol_id': volume['id'],
795+ 'host': connector['host'],
796+ 'iqn': iqn,
797+ 'pg': cfg['pg'].name
798+ })
799+ self._attach.api().iSCSIConfigChange({
800+ 'commands': [
801+ {
802+ 'export': {
803+ 'initiator': iqn,
804+ 'portalGroup': cfg['pg'].name,
805+ 'volumeName': cfg['volume_name'],
806+ },
807+ },
808+ ]
809+ })
810+
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200811+ target_portals = [
812+ "{addr}:3260".format(addr=net.address)
813+ for net in cfg['pg'].networks
814+ ]
815+ target_iqns = [cfg['target'].name] * len(target_portals)
816+ target_luns = [0] * len(target_portals)
817+ if connector.get('multipath', False):
818+ multipath_settings = {
819+ 'target_iqns': target_iqns,
820+ 'target_portals': target_portals,
821+ 'target_luns': target_luns,
822+ }
823+ else:
824+ multipath_settings = {}
825+
Peter Pentchev9c24be92022-09-26 22:35:24 +0300826+ res = {
827+ 'driver_volume_type': 'iscsi',
828+ 'data': {
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200829+ **multipath_settings,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300830+ 'target_discovered': False,
Peter Pentchevc53e6c02023-02-08 15:13:56 +0200831+ 'target_iqn': target_iqns[0],
832+ 'target_portal': target_portals[0],
833+ 'target_lun': target_luns[0],
Peter Pentchev9c24be92022-09-26 22:35:24 +0300834+ 'volume_id': volume['id'],
835+ 'discard': True,
836+ },
837+ }
838+ LOG.debug('returning %(res)s', {'res': res})
839+ return res
840+
841+ def _remove_iscsi_export(self, volume, connector):
842+ """Remove an iSCSI export for the specified StorPool volume."""
843+ LOG.debug(
844+ '_remove_iscsi_export() invoked for volume '
845+ '"%(vol_name)s" (%(vol_id)s) connector %(conn)s',
846+ {
847+ 'vol_name': volume['display_name'],
848+ 'vol_id': volume['id'],
849+ 'conn': connector,
850+ }
851+ )
852+ try:
853+ cfg = self._get_iscsi_config(connector['initiator'], volume['id'])
854+ except Exception as exc:
855+ LOG.error(
856+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
857+ )
858+ raise
859+
860+ if cfg['export'] is not None:
861+ LOG.info('Removing the StorPool iSCSI export '
862+ 'for the "%(vol_name)s" volume (%(vol_id)s) '
863+ 'to the "%(host)s" initiator (%(iqn)s) '
864+ 'in the "%(pg)s" portal group',
865+ {
866+ 'vol_name': volume['display_name'],
867+ 'vol_id': volume['id'],
868+ 'host': connector['host'],
869+ 'iqn': connector['initiator'],
870+ 'pg': cfg['pg'].name,
871+ })
872+ try:
873+ self._attach.api().iSCSIConfigChange({
874+ 'commands': [
875+ {
876+ 'exportDelete': {
877+ 'initiator': cfg['initiator'].name,
878+ 'portalGroup': cfg['pg'].name,
879+ 'volumeName': cfg['volume_name'],
880+ },
881+ },
882+ ]
883+ })
884+ except spapi.ApiError as e:
885+ if e.name not in ('objectExists', 'objectDoesNotExist'):
886+ raise
887+ LOG.info('Looks like somebody beat us to it')
888+
889+ if cfg['target'] is not None:
890+ last = True
891+ for initiator in cfg['cfg'].iscsi.initiators.values():
892+ if initiator.name == cfg['initiator'].name:
893+ continue
894+ for exp in initiator.exports:
895+ if exp.target == cfg['target'].name:
896+ last = False
897+ break
898+ if not last:
899+ break
900+
901+ if last:
902+ LOG.info(
903+ 'Removing the StorPool iSCSI target '
904+ 'for the "{vol_name}s" volume ({vol_id}s)',
905+ {
906+ 'vol_name': volume['display_name'],
907+ 'vol_id': volume['id'],
908+ }
909+ )
910+ try:
911+ self._attach.api().iSCSIConfigChange({
912+ 'commands': [
913+ {
914+ 'deleteTarget': {
915+ 'volumeName': cfg['volume_name'],
916+ },
917+ },
918+ ]
919+ })
920+ except spapi.ApiError as e:
921+ if e.name not in ('objectDoesNotExist', 'invalidParam'):
922+ raise
923+ LOG.info('Looks like somebody beat us to it')
924+
925 def initialize_connection(self, volume, connector):
926+ if self._connector_wants_iscsi(connector):
927+ return self._create_iscsi_export(volume, connector)
928 return {'driver_volume_type': 'storpool',
929 'data': {
930 'client_id': self._storpool_client_id(connector),
Peter Pentchevacaaa382023-02-28 11:26:13 +0200931@@ -174,6 +517,9 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300932 }}
933
934 def terminate_connection(self, volume, connector, **kwargs):
935+ if self._connector_wants_iscsi(connector):
936+ LOG.debug('- removing an iSCSI export')
937+ self._remove_iscsi_export(volume, connector)
938 pass
939
940 def create_snapshot(self, snapshot):
Peter Pentchevacaaa382023-02-28 11:26:13 +0200941@@ -275,11 +621,20 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300942 )
943
944 def create_export(self, context, volume, connector):
945- pass
946+ if self._connector_wants_iscsi(connector):
947+ LOG.debug('- creating an iSCSI export')
948+ self._create_iscsi_export(volume, connector)
949
950 def remove_export(self, context, volume):
951 pass
952
953+ def _attach_volume(self, context, volume, properties, remote=False):
954+ if self.configuration.iscsi_cinder_volume and not remote:
955+ LOG.debug('- adding the "storpool_wants_iscsi" flag')
956+ properties['storpool_wants_iscsi'] = True
957+
958+ return super()._attach_volume(context, volume, properties, remote)
959+
960 def delete_volume(self, volume):
961 name = self._attach.volumeName(volume['id'])
962 try:
Peter Pentchevacaaa382023-02-28 11:26:13 +0200963@@ -316,6 +671,17 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300964 LOG.error("StorPoolDriver API initialization failed: %s", e)
965 raise
966
967+ export_to = self.configuration.iscsi_export_to
968+ export_to_set = export_to is not None and export_to.split()
969+ vol_iscsi = self.configuration.iscsi_cinder_volume
970+ pg_name = self.configuration.iscsi_portal_group
971+ if (export_to_set or vol_iscsi) and pg_name is None:
972+ msg = _('The "iscsi_portal_group" option is required if '
973+ 'any patterns are listed in "iscsi_export_to"')
974+ raise exception.VolumeDriverException(message=msg)
975+
976+ self._use_iscsi = export_to == "*"
977+
978 def _update_volume_stats(self):
979 try:
980 dl = self._attach.api().disksList()
Peter Pentchevacaaa382023-02-28 11:26:13 +0200981@@ -341,7 +707,7 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300982 'total_capacity_gb': total / units.Gi,
983 'free_capacity_gb': free / units.Gi,
984 'reserved_percentage': 0,
985- 'multiattach': True,
Peter Pentchev5a9f8a62023-12-06 10:40:18 +0200986+ 'multiattach': self._use_iscsi,
Peter Pentchev9c24be92022-09-26 22:35:24 +0300987 'QoS_support': False,
988 'thick_provisioning_support': False,
989 'thin_provisioning_support': True,
Peter Pentchevacaaa382023-02-28 11:26:13 +0200990@@ -360,7 +726,9 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev9c24be92022-09-26 22:35:24 +0300991 'volume_backend_name') or 'storpool',
992 'vendor_name': 'StorPool',
993 'driver_version': self.VERSION,
994- 'storage_protocol': constants.STORPOOL,
995+ 'storage_protocol': (
996+ constants.ISCSI if self._use_iscsi else constants.STORPOOL
997+ ),
Peter Pentchevacaaa382023-02-28 11:26:13 +0200998 # Driver capabilities
Peter Pentchev9c24be92022-09-26 22:35:24 +0300999 'clone_across_pools': True,
1000 'sparse_copy_volume': True,
Peter Pentchevacaaa382023-02-28 11:26:13 +02001001diff --git a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
Peter Pentchevea354462023-07-18 11:15:56 +03001002index d2c5895a9..1f3d46cce 100644
Peter Pentchev9c24be92022-09-26 22:35:24 +03001003--- a/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
1004+++ b/doc/source/configuration/block-storage/drivers/storpool-volume-driver.rst
Peter Pentchevacaaa382023-02-28 11:26:13 +02001005@@ -19,12 +19,15 @@ Prerequisites
Peter Pentchev9c24be92022-09-26 22:35:24 +03001006 * The controller and all the compute nodes must have access to the StorPool
1007 API service.
1008
1009-* All nodes where StorPool-backed volumes will be attached must have access to
1010+* If iSCSI is not being used as a transport (see below), all nodes where
1011+ StorPool-backed volumes will be attached must have access to
1012 the StorPool data network and run the ``storpool_block`` service.
1013
1014-* If StorPool-backed Cinder volumes need to be created directly from Glance
1015- images, then the node running the ``cinder-volume`` service must also have
1016- access to the StorPool data network and run the ``storpool_block`` service.
1017+* If Glance uses Cinder as its image store, or if StorPool-backed Cinder
1018+ volumes need to be created directly from Glance images, and iSCSI is not
1019+ being used as a transport, then the node running the ``cinder-volume``
1020+ service must also have access to the StorPool data network and run
1021+ the ``storpool_block`` service.
1022
1023 * All nodes that need to access the StorPool API (the compute nodes and
1024 the node running the ``cinder-volume`` service) must have the following
Peter Pentchevacaaa382023-02-28 11:26:13 +02001025@@ -34,6 +37,29 @@ Prerequisites
Peter Pentchev9c24be92022-09-26 22:35:24 +03001026 * the storpool Python bindings package
1027 * the storpool.spopenstack Python helper package
1028
1029+Using iSCSI as the transport protocol
1030+-------------------------------------
1031+
1032+The StorPool distributed storage system uses its own, highly optimized and
1033+tailored for its specifics, network protocol for communication between
1034+the storage servers and the clients (the OpenStack cluster nodes where
1035+StorPool-backed volumes will be attached). There are cases when granting
1036+various nodes access to the StorPool data network or installing and
1037+running the ``storpool_block`` client service on them may pose difficulties.
1038+The StorPool servers may also expose the user-created volumes and snapshots
1039+using the standard iSCSI protocol that only requires TCP routing and
1040+connectivity between the storage servers and the StorPool clients.
1041+The StorPool Cinder driver may be configured to export volumes and
1042+snapshots via iSCSI using the ``iscsi_export_to`` and ``iscsi_portal_group``
1043+configuration options.
1044+
1045+Additionally, even if e.g. the hypervisor nodes running Nova will use
1046+the StorPool network protocol and run the ``storpool_block`` service
1047+(so the ``iscsi_export_to`` option has its default empty string value),
1048+the ``iscsi_cinder_volume`` option configures the StorPool Cinder driver
1049+so that only the ``cinder-volume`` service will use the iSCSI protocol when
1050+attaching volumes and snapshots to transfer data to and from Glance images.
1051+
1052 Configuring the StorPool volume driver
1053 --------------------------------------
1054
Peter Pentchevea354462023-07-18 11:15:56 +03001055@@ -55,6 +81,32 @@ volume backend definition) and per volume type:
Peter Pentchev9c24be92022-09-26 22:35:24 +03001056 with the default placement constraints for the StorPool cluster.
1057 The default value for the chain replication is 3.
1058
Peter Pentchevea354462023-07-18 11:15:56 +03001059+In addition, if the iSCSI protocol is used to access the StorPool cluster as
1060+described in the previous section, the following options may be defined in
1061+the ``cinder.conf`` volume backend definition:
1062+
Peter Pentchev9c24be92022-09-26 22:35:24 +03001063+- ``iscsi_export_to``: if set to the value ``*``, the StorPool Cinder driver
1064+ will export volumes and snapshots using the iSCSI protocol instead of
1065+ the StorPool network protocol. The ``iscsi_portal_group`` option must also
1066+ be specified.
1067+
1068+- ``iscsi_portal_group``: if the ``iscsi_export_to`` option is set to
1069+ the value ``*`` or the ``iscsi_cinder_volume`` option is turned on,
1070+ this option specifies the name of the iSCSI portal group that Cinder
1071+ volumes will be exported to.
1072+
1073+- ``iscsi_cinder_volume``: if enabled, even if the ``iscsi_export_to`` option
1074+ has its default empty value, the ``cinder-volume`` service will use iSCSI
1075+ to attach the volumes and snapshots for transferring data to and from
1076+ Glance images.
1077+
Peter Pentchevea354462023-07-18 11:15:56 +03001078+- ``iscsi_learn_initiator_iqns``: if enabled, the StorPool Cinder driver will
1079+ automatically use the StorPool API to create definitions for new initiators
1080+ in the StorPool cluster's configuration. This is the default behavior of
1081+ the driver; it may be disabled in the rare case if, e.g. because of site
1082+ policy, OpenStack iSCSI initiators (e.g. Nova hypervisors) need to be
1083+ explicitly allowed to use the StorPool iSCSI targets.
1084+
Peter Pentchev9c24be92022-09-26 22:35:24 +03001085 Using the StorPool volume driver
1086 --------------------------------
1087
Peter Pentchevea354462023-07-18 11:15:56 +03001088diff --git a/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml b/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
1089new file mode 100644
Peter Pentchev5a9f8a62023-12-06 10:40:18 +02001090index 000000000..edf46d298
Peter Pentchevea354462023-07-18 11:15:56 +03001091--- /dev/null
1092+++ b/releasenotes/notes/storpool-iscsi-cefcfe590a07c5c7.yaml
Peter Pentchev5a9f8a62023-12-06 10:40:18 +02001093@@ -0,0 +1,13 @@
Peter Pentchevea354462023-07-18 11:15:56 +03001094+---
1095+features:
1096+ - |
1097+ StorPool driver: Added support for exporting the StorPool-backed volumes
1098+ using the iSCSI protocol, so that the Cinder volume service and/or
1099+ the Nova or Glance consumers do not need to have the StorPool block
1100+ device third-party service installed. See the StorPool driver section in
1101+ the Cinder documentation for more information on the ``iscsi_export_to``,
1102+ ``iscsi_portal_group``, ``iscsi_cinder_volume``, and
1103+ ``iscsi_learn_initiator_iqns`` options.
Peter Pentchev5a9f8a62023-12-06 10:40:18 +02001104+ Note that multiattach support for StorPool is now only enabled if
1105+ ``iscsi_export_to`` is set to ``*`, that is, all StorPool volumes will be
1106+ exported via iSCSI to all initiators.
Peter Pentchevacaaa382023-02-28 11:26:13 +02001107--
Peter Pentchev5a9f8a62023-12-06 10:40:18 +020011082.42.0
Peter Pentchevacaaa382023-02-28 11:26:13 +02001109