blob: aade52839ec0acf333c2b1b976a101d2446e0048 [file] [log] [blame]
Peter Pentcheva102cc52021-11-30 16:10:27 +02001From 48844314b876fd8f6983b457ba1e22e6e845a818 Mon Sep 17 00:00:00 2001
Peter Pentchevd0130fb2021-11-30 10:50:05 +02002From: Peter Penchev <openstack-dev@storpool.com>
3Date: Mon, 12 Mar 2018 12:00:10 +0200
4Subject: [PATCH] Add iSCSI export support to the StorPool driver.
5
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: a list of IQN patterns that the driver should export
11 volumes to using iSCSI and not the native StorPool protocol
12- iscsi_portal_group: the name of the iSCSI portal group defined in
13 the StorPool configuration to use for these export
14- iscsi_learn_initiator_iqns: automatically create StorPool configuration
15 records for an initiator when a volume is first exported to it
16
17Change-Id: I9de64306e0e6976268df782053b0651dd1cca96f
18---
19 .../unit/volume/drivers/test_storpool.py | 7 +-
20 cinder/volume/drivers/storpool.py | 354 +++++++++++++++++-
21 2 files changed, 357 insertions(+), 4 deletions(-)
22
23diff --git a/cinder/tests/unit/volume/drivers/test_storpool.py b/cinder/tests/unit/volume/drivers/test_storpool.py
24index 843283db4..df57dee5d 100644
25--- a/cinder/tests/unit/volume/drivers/test_storpool.py
26+++ b/cinder/tests/unit/volume/drivers/test_storpool.py
27@@ -181,6 +181,9 @@ class StorPoolTestCase(test.TestCase):
28 self.cfg.volume_backend_name = 'storpool_test'
29 self.cfg.storpool_template = None
30 self.cfg.storpool_replication = 3
31+ self.cfg.iscsi_cinder_volume = False
32+ self.cfg.iscsi_export_to = ''
33+ self.cfg.iscsi_portal_group = 'test-group'
34
35 mock_exec = mock.Mock()
36 mock_exec.return_value = ('', '')
37@@ -190,7 +193,7 @@ class StorPoolTestCase(test.TestCase):
38 self.driver.check_for_setup_error()
39
40 @ddt.data(
41- (5, TypeError),
42+ (5, (TypeError, AttributeError)),
43 ({'no-host': None}, KeyError),
44 ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
45 ({'host': 's01'}, None),
46@@ -206,7 +209,7 @@ class StorPoolTestCase(test.TestCase):
47 conn)
48
49 @ddt.data(
50- (5, TypeError),
51+ (5, (TypeError, AttributeError)),
52 ({'no-host': None}, KeyError),
53 ({'host': 'sbad'}, driver.StorPoolConfigurationInvalid),
54 )
55diff --git a/cinder/volume/drivers/storpool.py b/cinder/volume/drivers/storpool.py
Peter Pentcheva102cc52021-11-30 16:10:27 +020056index 0d2903684..7ab46e2a3 100644
Peter Pentchevd0130fb2021-11-30 10:50:05 +020057--- a/cinder/volume/drivers/storpool.py
58+++ b/cinder/volume/drivers/storpool.py
59@@ -15,6 +15,7 @@
60
61 """StorPool block device driver"""
62
63+import fnmatch
64 import platform
65
66 from oslo_config import cfg
67@@ -41,6 +42,24 @@ if storpool:
68
69
70 storpool_opts = [
71+ cfg.BoolOpt('iscsi_cinder_volume',
72+ default=False,
73+ help='Let the cinder-volume service use iSCSI instead of '
74+ 'the StorPool block device driver for accessing '
75+ 'StorPool volumes, e.g. when creating a volume from '
76+ 'an image or vice versa.'),
77+ cfg.StrOpt('iscsi_export_to',
78+ default='',
79+ help='Export volumes via iSCSI to the hosts with IQNs that '
80+ 'match the patterns in this list, e.g. '
81+ '"iqn.1991-05.com.microsoft:*" for Windows hosts'),
82+ cfg.BoolOpt('iscsi_learn_initiator_iqns',
83+ default=True,
84+ help='Create a StorPool record for a new initiator as soon as '
85+ 'Cinder asks for a volume to be exported to it.'),
86+ cfg.StrOpt('iscsi_portal_group',
87+ default=None,
88+ help='The portal group to export volumes via iSCSI in.'),
89 cfg.StrOpt('storpool_template',
90 default=None,
91 help='The StorPool template for volumes with no type.'),
92@@ -92,9 +111,10 @@ class StorPoolDriver(driver.VolumeDriver):
93 1.2.3 - Advertise some more driver capabilities.
94 2.0.0 - Drop _attach_volume() and _detach_volume(), our os-brick
95 connector will handle this.
96+ - Add support for exporting volumes via iSCSI.
97 """
98
99- VERSION = '1.2.3'
100+ VERSION = '2.0.0'
101 CI_WIKI_NAME = 'StorPool_distributed_storage_CI'
102
103 def __init__(self, *args, **kwargs):
104@@ -161,10 +181,319 @@ class StorPoolDriver(driver.VolumeDriver):
105 raise StorPoolConfigurationInvalid(
106 section=hostname, param='SP_OURID', error=e)
107
108+ def _connector_wants_iscsi(self, connector):
109+ """Should we do this export via iSCSI?
110+
111+ Check the configuration to determine whether this connector is
112+ expected to provide iSCSI exports as opposed to native StorPool
113+ protocol ones. Match the initiator's IQN against the list of
114+ patterns supplied in the "iscsi_export_to" configuration setting.
115+ """
116+ if connector is None:
117+ return False
118+ if connector.get('storpool_wants_iscsi'):
119+ LOG.debug(' - forcing iSCSI for the controller')
120+ return True
121+
122+ try:
123+ iqn = connector.get('initiator')
124+ except Exception:
125+ iqn = None
126+ try:
127+ host = connector.get('host')
128+ except Exception:
129+ host = None
130+ if iqn is None or host is None:
131+ LOG.debug(' - this connector certainly does not want iSCSI')
132+ return False
133+
134+ LOG.debug(' - check whether %(host)s (%(iqn)s) wants iSCSI',
135+ {
136+ 'host': host,
137+ 'iqn': iqn,
138+ })
139+
140+ export_to = self.configuration.iscsi_export_to
141+ if export_to is None:
142+ return False
143+
144+ for pat in export_to.split():
145+ LOG.debug(' - matching against %(pat)s', {'pat': pat})
146+ if fnmatch.fnmatch(iqn, pat):
147+ LOG.debug(' - got it!')
148+ return True
149+ LOG.debug(' - nope')
150+ return False
151+
152 def validate_connector(self, connector):
153+ if self._connector_wants_iscsi(connector):
154+ return True
155 return self._storpool_client_id(connector) >= 0
156
157+ def _get_iscsi_config(self, iqn, volume_id):
158+ """Get the StorPool iSCSI config items pertaining to this volume.
159+
160+ Find the elements of the StorPool iSCSI configuration tree that
161+ will be needed to create, ensure, or remove the iSCSI export of
162+ the specified volume to the specified initiator.
163+ """
164+ cfg = self._attach.api().iSCSIConfig()
165+
166+ pg_name = self.configuration.iscsi_portal_group
167+ pg_found = [
168+ pg for pg in cfg.iscsi.portalGroups.values() if pg.name == pg_name
169+ ]
170+ if not pg_found:
171+ raise Exception('StorPool Cinder iSCSI configuration error: '
172+ 'no portal group "{pg}"'.format(pg=pg_name))
173+ pg = pg_found[0]
174+
175+ # Do we know about this initiator?
176+ i_found = [
177+ init for init in cfg.iscsi.initiators.values() if init.name == iqn
178+ ]
179+ if i_found:
180+ initiator = i_found[0]
181+ else:
182+ initiator = None
183+
184+ # Is this volume already being exported?
185+ volname = self._attach.volumeName(volume_id)
186+ t_found = [
187+ tgt for tgt in cfg.iscsi.targets.values() if tgt.volume == volname
188+ ]
189+ if t_found:
190+ target = t_found[0]
191+ else:
192+ target = None
193+
194+ # OK, so is this volume being exported to this initiator?
195+ export = None
196+ if initiator is not None and target is not None:
197+ e_found = [
198+ exp for exp in initiator.exports
199+ if exp.portalGroup == pg.name and exp.target == target.name
200+ ]
201+ if e_found:
202+ export = e_found[0]
203+
204+ return {
205+ 'cfg': cfg,
206+ 'pg': pg,
207+ 'initiator': initiator,
208+ 'target': target,
209+ 'export': export,
210+ 'volume_name': volname,
211+ 'volume_id': volume_id,
212+ }
213+
214+ def _create_iscsi_export(self, volume, connector):
215+ """Create (if needed) an iSCSI export for the StorPool volume."""
216+ LOG.debug(
217+ '_create_iscsi_export() invoked for volume '
218+ '"%(vol_name)s" (%(vol_id)s) connector %(connector)s',
219+ {
220+ 'vol_name': volume['display_name'],
221+ 'vol_id': volume['id'],
222+ 'connector': connector,
223+ }
224+ )
225+ iqn = connector['initiator']
226+ try:
227+ cfg = self._get_iscsi_config(iqn, volume['id'])
228+ except Exception as exc:
229+ LOG.error(
230+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
231+ )
232+ raise
233+
234+ if cfg['initiator'] is None:
235+ LOG.info(
236+ 'RDBG initiator? learn %(learn)s vol %(vol)s want %(want)s',
237+ {
238+ 'learn': repr(
239+ self.configuration.iscsi_learn_initiator_iqns
240+ ),
241+ 'vol': repr(self.configuration.iscsi_cinder_volume),
242+ 'want': repr(connector.get('storpool_wants_iscsi')),
243+ }
244+ )
245+ if not (self.configuration.iscsi_learn_initiator_iqns or
246+ self.configuration.iscsi_cinder_volume and
247+ connector.get('storpool_wants_iscsi')):
248+ raise Exception('The "{iqn}" initiator IQN for the "{host}" '
249+ 'host is not defined in the StorPool '
250+ 'configuration.'
251+ .format(iqn=iqn, host=connector['host']))
252+ else:
253+ LOG.info('Creating a StorPool iSCSI initiator '
254+ 'for "{host}s" ({iqn}s)',
255+ {'host': connector['host'], 'iqn': iqn})
256+ self._attach.api().iSCSIConfigChange({
257+ 'commands': [
258+ {
259+ 'createInitiator': {
260+ 'name': iqn,
261+ 'username': '',
262+ 'secret': '',
263+ },
264+ },
265+ {
266+ 'initiatorAddNetwork': {
267+ 'initiator': iqn,
268+ 'net': '0.0.0.0/0',
269+ },
270+ },
271+ ]
272+ })
273+
274+ if cfg['target'] is None:
275+ LOG.info(
276+ 'Creating a StorPool iSCSI target '
277+ 'for the "%(vol_name)s" volume (%(vol_id)s)',
278+ {
279+ 'vol_name': volume['display_name'],
280+ 'vol_id': volume['id'],
281+ }
282+ )
283+ self._attach.api().iSCSIConfigChange({
284+ 'commands': [
285+ {
286+ 'createTarget': {
287+ 'volumeName': cfg['volume_name'],
288+ },
289+ },
290+ ]
291+ })
292+ cfg = self._get_iscsi_config(iqn, volume['id'])
293+
294+ if cfg['export'] is None:
295+ LOG.info('Creating a StorPool iSCSI export '
296+ 'for the "{vol_name}s" volume ({vol_id}s) '
297+ 'to the "{host}s" initiator ({iqn}s) '
298+ 'in the "{pg}s" portal group',
299+ {
300+ 'vol_name': volume['display_name'],
301+ 'vol_id': volume['id'],
302+ 'host': connector['host'],
303+ 'iqn': iqn,
304+ 'pg': cfg['pg'].name
305+ })
306+ self._attach.api().iSCSIConfigChange({
307+ 'commands': [
308+ {
309+ 'export': {
310+ 'initiator': iqn,
311+ 'portalGroup': cfg['pg'].name,
312+ 'volumeName': cfg['volume_name'],
313+ },
314+ },
315+ ]
316+ })
317+
318+ res = {
319+ 'driver_volume_type': 'iscsi',
320+ 'data': {
321+ 'target_discovered': False,
322+ 'target_iqn': cfg['target'].name,
323+ 'target_portal': '{}:3260'.format(
324+ cfg['pg'].networks[0].address
325+ ),
326+ 'target_lun': 0,
327+ 'volume_id': volume['id'],
328+ 'discard': True,
329+ },
330+ }
331+ LOG.debug('returning %(res)s', {'res': res})
332+ return res
333+
334+ def _remove_iscsi_export(self, volume, connector):
335+ """Remove an iSCSI export for the specified StorPool volume."""
336+ LOG.debug(
337+ '_remove_iscsi_export() invoked for volume '
338+ '"%(vol_name)s" (%(vol_id)s) connector %(conn)s',
339+ {
340+ 'vol_name': volume['display_name'],
341+ 'vol_id': volume['id'],
342+ 'conn': connector,
343+ }
344+ )
345+ try:
346+ cfg = self._get_iscsi_config(connector['initiator'], volume['id'])
347+ except Exception as exc:
348+ LOG.error(
349+ 'Could not fetch the iSCSI config: %(exc)s', {'exc': exc}
350+ )
351+ raise
352+
353+ if cfg['export'] is not None:
354+ LOG.info('Removing the StorPool iSCSI export '
355+ 'for the "%(vol_name)s" volume (%(vol_id)s) '
356+ 'to the "%(host)s" initiator (%(iqn)s) '
357+ 'in the "%(pg)s" portal group',
358+ {
359+ 'vol_name': volume['display_name'],
360+ 'vol_id': volume['id'],
361+ 'host': connector['host'],
362+ 'iqn': connector['initiator'],
363+ 'pg': cfg['pg'].name,
364+ })
365+ try:
366+ self._attach.api().iSCSIConfigChange({
367+ 'commands': [
368+ {
369+ 'exportDelete': {
370+ 'initiator': cfg['initiator'].name,
371+ 'portalGroup': cfg['pg'].name,
372+ 'volumeName': cfg['volume_name'],
373+ },
374+ },
375+ ]
376+ })
377+ except spapi.ApiError as e:
378+ if e.name not in ('objectExists', 'objectDoesNotExist'):
379+ raise
380+ LOG.info('Looks like somebody beat us to it')
381+
382+ if cfg['target'] is not None:
383+ last = True
384+ for initiator in cfg['cfg'].iscsi.initiators.values():
385+ if initiator.name == cfg['initiator'].name:
386+ continue
387+ for exp in initiator.exports:
388+ if exp.target == cfg['target'].name:
389+ last = False
390+ break
391+ if not last:
392+ break
393+
394+ if last:
395+ LOG.info(
396+ 'Removing the StorPool iSCSI target '
397+ 'for the "{vol_name}s" volume ({vol_id}s)',
398+ {
399+ 'vol_name': volume['display_name'],
400+ 'vol_id': volume['id'],
401+ }
402+ )
403+ try:
404+ self._attach.api().iSCSIConfigChange({
405+ 'commands': [
406+ {
407+ 'deleteTarget': {
408+ 'volumeName': cfg['volume_name'],
409+ },
410+ },
411+ ]
412+ })
413+ except spapi.ApiError as e:
414+ if e.name not in ('objectDoesNotExist', 'invalidParam'):
415+ raise
416+ LOG.info('Looks like somebody beat us to it')
417+
418 def initialize_connection(self, volume, connector):
419+ if self._connector_wants_iscsi(connector):
420+ return self._create_iscsi_export(volume, connector)
421 return {'driver_volume_type': 'storpool',
422 'data': {
423 'client_id': self._storpool_client_id(connector),
424@@ -173,6 +502,9 @@ class StorPoolDriver(driver.VolumeDriver):
425 }}
426
427 def terminate_connection(self, volume, connector, **kwargs):
428+ if self._connector_wants_iscsi(connector):
429+ LOG.debug('- removing an iSCSI export')
430+ self._remove_iscsi_export(volume, connector)
431 pass
432
433 def create_snapshot(self, snapshot):
434@@ -224,11 +556,20 @@ class StorPoolDriver(driver.VolumeDriver):
435 {'name': snapname, 'msg': e})
436
437 def create_export(self, context, volume, connector):
438- pass
439+ if self._connector_wants_iscsi(connector):
440+ LOG.debug('- creating an iSCSI export')
441+ self._create_iscsi_export(volume, connector)
442
443 def remove_export(self, context, volume):
444 pass
445
446+ def _attach_volume(self, context, volume, properties, remote=False):
Peter Pentcheva102cc52021-11-30 16:10:27 +0200447+ if self.configuration.iscsi_cinder_volume and not remote:
Peter Pentchevd0130fb2021-11-30 10:50:05 +0200448+ LOG.debug('- adding the "storpool_wants_iscsi" flag')
449+ properties['storpool_wants_iscsi'] = True
450+
451+ return super()._attach_volume(context, volume, properties, remote)
452+
453 def delete_volume(self, volume):
454 name = self._attach.volumeName(volume['id'])
455 try:
456@@ -265,6 +606,15 @@ class StorPoolDriver(driver.VolumeDriver):
457 LOG.error("StorPoolDriver API initialization failed: %s", e)
458 raise
459
460+ export_to = self.configuration.iscsi_export_to
461+ export_to_set = export_to is not None and export_to.split()
462+ vol_iscsi = self.configuration.iscsi_cinder_volume
463+ pg_name = self.configuration.iscsi_portal_group
464+ if (export_to_set or vol_iscsi) and pg_name is None:
465+ msg = _('The "iscsi_portal_group" option is required if '
466+ 'any patterns are listed in "iscsi_export_to"')
467+ raise exception.VolumeDriverException(message=msg)
468+
469 def _update_volume_stats(self):
470 try:
471 dl = self._attach.api().disksList()
472--
4732.33.0
474