Merge lp:~markmc/nova/iscsi-tgtadm-choice into lp:~hudson-openstack/nova/trunk

Proposed by Mark McLoughlin
Status: Work in progress
Proposed branch: lp:~markmc/nova/iscsi-tgtadm-choice
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 553 lines (+331/-76)
4 files modified
nova/tests/test_iscsi.py (+116/-0)
nova/tests/test_volume.py (+6/-11)
nova/volume/driver.py (+53/-65)
nova/volume/iscsi.py (+156/-0)
To merge this branch: bzr merge lp:~markmc/nova/iscsi-tgtadm-choice
Reviewer Review Type Date Requested Status
Christopher MacGown (community) Approve
Review via email: mp+75906@code.launchpad.net

Commit message

Add --iscsi_helper flag and support tgtadm (lp:819997)

The iSCSI enterprise target project (aka iet) never went into the upstream kernel and never got into Fedora.

On the other hand, the tgt project did get its kernel module upstream and its userland packaged in Fedora as scsi-target-utils. It seems that tgt is the preferred option for Ubuntu too.

Nova upstream uses ietadm from the former, but since this isn't available in Fedora we need the option of using tgtadm instead.

This merge adds an --iscsi_helper flag which can be set to ietadm or tgtadm. The former is used by default to preserve the current behaviour.

To post a comment you must log in.
Revision history for this message
Chuck Short (zulcss) wrote :

lgtm

Revision history for this message
Christopher MacGown (0x44) wrote :

lgtm, also kudos for clean and understandable tests.

review: Approve
Revision history for this message
Mark McLoughlin (markmc) wrote :

Unmerged revisions

1422. By Mark McLoughlin

Refactor ietadm/tgtadm calls out into helper classes

Add a new TargetAdmin abstract base class and implement it using ietadm
and tgtadm. This cleans up the code greatly and gets us some code reuse.

1421. By Mark McLoughlin

Remove VolumeDriver.sync_exec method

We always use the same functions for sync_exec and execute.

The execute method is always synchronous, so the distinction doesn't
appear to make sense.

Finally, it looks like it would make sense for execute to ever be
async, so the distinction isn't even serving a useful documentation
purpose.

1420. By Mark McLoughlin

Fix pep8 issues

1419. By Mark McLoughlin

Merge trunk

1418. By Chuck Short

Fix pep8 errors

1417. By Chuck Short

Fixup thinkos

1416. By Chuck Short

Allow the user to choose either ietadm or tgtadm

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'nova/tests/test_iscsi.py'
2--- nova/tests/test_iscsi.py 1970-01-01 00:00:00 +0000
3+++ nova/tests/test_iscsi.py 2011-09-18 15:48:23 +0000
4@@ -0,0 +1,116 @@
5+# vim: tabstop=4 shiftwidth=4 softtabstop=4
6+
7+# Copyright 2011 Red Hat, Inc.
8+#
9+# Licensed under the Apache License, Version 2.0 (the "License"); you may
10+# not use this file except in compliance with the License. You may obtain
11+# a copy of the License at
12+#
13+# http://www.apache.org/licenses/LICENSE-2.0
14+#
15+# Unless required by applicable law or agreed to in writing, software
16+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
17+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
18+# License for the specific language governing permissions and limitations
19+# under the License.
20+
21+import string
22+
23+from nova import test
24+from nova.volume import iscsi
25+
26+
27+class TargetAdminTestCase(object):
28+
29+ def setUp(self):
30+ self.cmds = []
31+
32+ self.tid = 1
33+ self.target_name = 'iqn.2011-09.org.foo.bar:blaa'
34+ self.lun = 10
35+ self.path = '/foo/bar/blaa'
36+
37+ self.script_template = None
38+
39+ def get_script_params(self):
40+ return {'tid': self.tid,
41+ 'target_name': self.target_name,
42+ 'lun': self.lun,
43+ 'path': self.path}
44+
45+ def get_script(self):
46+ return self.script_template % self.get_script_params()
47+
48+ def fake_execute(self, *cmd, **kwargs):
49+ self.cmds.append(string.join(cmd))
50+ return "", None
51+
52+ def clear_cmds(self):
53+ cmds = []
54+
55+ def verify_cmds(self, cmds):
56+ self.assertEqual(len(cmds), len(self.cmds))
57+ for a, b in zip(cmds, self.cmds):
58+ self.assertEqual(a, b)
59+
60+ def verify(self):
61+ script = self.get_script()
62+ cmds = []
63+ for line in script.split('\n'):
64+ if not line.strip():
65+ continue
66+ cmds.append(line)
67+ self.verify_cmds(cmds)
68+
69+ def run_commands(self):
70+ tgtadm = iscsi.get_target_admin()
71+ tgtadm.set_execute(self.fake_execute)
72+ tgtadm.new_target(self.target_name, self.tid)
73+ tgtadm.show_target(self.tid)
74+ tgtadm.new_logicalunit(self.tid, self.lun, self.path)
75+ tgtadm.delete_logicalunit(self.tid, self.lun)
76+ tgtadm.delete_target(self.tid)
77+
78+ def test_target_admin(self):
79+ self.clear_cmds()
80+ self.run_commands()
81+ self.verify()
82+
83+
84+class TgtAdmTestCase(test.TestCase, TargetAdminTestCase):
85+
86+ def setUp(self):
87+ super(TgtAdmTestCase, self).setUp()
88+ TargetAdminTestCase.setUp(self)
89+ self.flags(iscsi_helper='tgtadm')
90+ self.script_template = """
91+tgtadm --op new --lld=iscsi --mode=target --tid=%(tid)s \
92+--targetname=%(target_name)s
93+tgtadm --op bind --lld=iscsi --mode=target --initiator-address=ALL \
94+--tid=%(tid)s
95+tgtadm --op show --lld=iscsi --mode=target --tid=%(tid)s
96+tgtadm --op new --lld=iscsi --mode=logicalunit --tid=%(tid)s --lun=%(lun)d \
97+--backing-store=%(path)s
98+tgtadm --op delete --lld=iscsi --mode=logicalunit --tid=%(tid)s --lun=%(lun)d
99+tgtadm --op delete --lld=iscsi --mode=target --tid=%(tid)s
100+"""
101+
102+ def get_script_params(self):
103+ params = super(TgtAdmTestCase, self).get_script_params()
104+ params['lun'] += 1
105+ return params
106+
107+
108+class IetAdmTestCase(test.TestCase, TargetAdminTestCase):
109+
110+ def setUp(self):
111+ super(IetAdmTestCase, self).setUp()
112+ TargetAdminTestCase.setUp(self)
113+ self.flags(iscsi_helper='ietadm')
114+ self.script_template = """
115+ietadm --op new --tid=%(tid)s --params Name=%(target_name)s
116+ietadm --op show --tid=%(tid)s
117+ietadm --op new --tid=%(tid)s --lun=%(lun)d --params Path=%(path)s,Type=fileio
118+ietadm --op delete --tid=%(tid)s --lun=%(lun)d
119+ietadm --op delete --tid=%(tid)s
120+"""
121
122=== modified file 'nova/tests/test_volume.py'
123--- nova/tests/test_volume.py 2011-08-05 14:23:48 +0000
124+++ nova/tests/test_volume.py 2011-09-18 15:48:23 +0000
125@@ -270,8 +270,7 @@
126 def _fake_execute(_command, *_args, **_kwargs):
127 """Fake _execute."""
128 return self.output, None
129- self.volume.driver._execute = _fake_execute
130- self.volume.driver._sync_execute = _fake_execute
131+ self.volume.driver.set_execute(_fake_execute)
132
133 log = logging.getLogger()
134 self.stream = cStringIO.StringIO()
135@@ -411,12 +410,10 @@
136 """No log message when all the vblade processes are running."""
137 volume_id_list = self._attach_volume()
138
139- self.mox.StubOutWithMock(self.volume.driver, '_execute')
140+ self.mox.StubOutWithMock(self.volume.driver.tgtadm, 'show_target')
141 for i in volume_id_list:
142 tid = db.volume_get_iscsi_target_num(self.context, i)
143- self.volume.driver._execute("ietadm", "--op", "show",
144- "--tid=%(tid)d" % locals(),
145- run_as_root=True)
146+ self.volume.driver.tgtadm.show_target(tid)
147
148 self.stream.truncate(0)
149 self.mox.ReplayAll()
150@@ -433,11 +430,9 @@
151
152 # the first vblade process isn't running
153 tid = db.volume_get_iscsi_target_num(self.context, volume_id_list[0])
154- self.mox.StubOutWithMock(self.volume.driver, '_execute')
155- self.volume.driver._execute("ietadm", "--op", "show",
156- "--tid=%(tid)d" % locals(),
157- run_as_root=True).AndRaise(
158- exception.ProcessExecutionError())
159+ self.mox.StubOutWithMock(self.volume.driver.tgtadm, 'show_target')
160+ self.volume.driver.tgtadm.show_target(tid).AndRaise(
161+ exception.ProcessExecutionError())
162
163 self.mox.ReplayAll()
164 self.assertRaises(exception.ProcessExecutionError,
165
166=== modified file 'nova/volume/driver.py'
167--- nova/volume/driver.py 2011-09-13 21:32:24 +0000
168+++ nova/volume/driver.py 2011-09-18 15:48:23 +0000
169@@ -28,6 +28,7 @@
170 from nova import flags
171 from nova import log as logging
172 from nova import utils
173+from nova.volume import iscsi
174 from nova.volume import volume_types
175
176
177@@ -60,12 +61,13 @@
178
179 class VolumeDriver(object):
180 """Executes commands relating to Volumes."""
181- def __init__(self, execute=utils.execute,
182- sync_exec=utils.execute, *args, **kwargs):
183+ def __init__(self, execute=utils.execute, *args, **kwargs):
184 # NOTE(vish): db is set by Manager
185 self.db = None
186+ self.set_execute(execute)
187+
188+ def set_execute(self, execute):
189 self._execute = execute
190- self._sync_exec = sync_exec
191
192 def _try_execute(self, *command, **kwargs):
193 # NOTE(vish): Volume commands can partially fail due to timing, but
194@@ -328,7 +330,6 @@
195
196 def __init__(self, *args, **kwargs):
197 super(FakeAOEDriver, self).__init__(execute=self.fake_execute,
198- sync_exec=self.fake_execute,
199 *args, **kwargs)
200
201 def check_for_setup_error(self):
202@@ -356,6 +357,14 @@
203 `CHAP` is the only auth_method in use at the moment.
204 """
205
206+ def __init__(self, *args, **kwargs):
207+ self.tgtadm = iscsi.get_target_admin()
208+ super(ISCSIDriver, self).__init__(*args, **kwargs)
209+
210+ def set_execute(self, execute):
211+ super(ISCSIDriver, self).set_execute(execute)
212+ self.tgtadm.set_execute(execute)
213+
214 def ensure_export(self, context, volume):
215 """Synchronously recreates an export for a logical volume."""
216 try:
217@@ -368,19 +377,10 @@
218
219 iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name'])
220 volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name'])
221- self._sync_exec('ietadm', '--op', 'new',
222- "--tid=%s" % iscsi_target,
223- '--params',
224- "Name=%s" % iscsi_name,
225- run_as_root=True,
226- check_exit_code=False)
227- self._sync_exec('ietadm', '--op', 'new',
228- "--tid=%s" % iscsi_target,
229- '--lun=0',
230- '--params',
231- "Path=%s,Type=fileio" % volume_path,
232- run_as_root=True,
233- check_exit_code=False)
234+
235+ self.tgtadm.new_target(iscsi_name, iscsi_target, check_exit_code=False)
236+ self.tgtadm.new_logicalunit(iscsi_target, 0, volume_path,
237+ check_exit_code=False)
238
239 def _ensure_iscsi_targets(self, context, host):
240 """Ensure that target ids have been created in datastore."""
241@@ -400,13 +400,9 @@
242 volume['host'])
243 iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name'])
244 volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name'])
245- self._execute('ietadm', '--op', 'new',
246- '--tid=%s' % iscsi_target,
247- '--params', 'Name=%s' % iscsi_name, run_as_root=True)
248- self._execute('ietadm', '--op', 'new',
249- '--tid=%s' % iscsi_target,
250- '--lun=0', '--params',
251- 'Path=%s,Type=fileio' % volume_path, run_as_root=True)
252+
253+ self.tgtadm.new_target(iscsi_name, iscsi_target)
254+ self.tgtadm.new_logicalunit(iscsi_target, 0, volume_path)
255
256 def remove_export(self, context, volume):
257 """Removes an export for a logical volume."""
258@@ -421,18 +417,14 @@
259 try:
260 # ietadm show will exit with an error
261 # this export has already been removed
262- self._execute('ietadm', '--op', 'show',
263- '--tid=%s' % iscsi_target, run_as_root=True)
264+ self.tgtadm.show_target(iscsi_target)
265 except Exception as e:
266 LOG.info(_("Skipping remove_export. No iscsi_target " +
267 "is presently exported for volume: %d"), volume['id'])
268 return
269
270- self._execute('ietadm', '--op', 'delete',
271- '--tid=%s' % iscsi_target,
272- '--lun=0', run_as_root=True)
273- self._execute('ietadm', '--op', 'delete',
274- '--tid=%s' % iscsi_target, run_as_root=True)
275+ self.tgtadm.delete_logicalunit(iscsi_target, 0)
276+ self.tgtadm.delete_target(iscsi_target)
277
278 def _do_iscsi_discovery(self, volume):
279 #TODO(justinsb): Deprecate discovery and use stored info
280@@ -583,8 +575,7 @@
281
282 tid = self.db.volume_get_iscsi_target_num(context, volume_id)
283 try:
284- self._execute('ietadm', '--op', 'show',
285- '--tid=%(tid)d' % locals(), run_as_root=True)
286+ self.tgtadm.show_target(tid)
287 except exception.ProcessExecutionError, e:
288 # Instances remount read-only in this case.
289 # /etc/init.d/iscsitarget restart and rebooting nova-volume
290@@ -598,7 +589,6 @@
291 """Logs calls instead of executing."""
292 def __init__(self, *args, **kwargs):
293 super(FakeISCSIDriver, self).__init__(execute=self.fake_execute,
294- sync_exec=self.fake_execute,
295 *args, **kwargs)
296
297 def check_for_setup_error(self):
298@@ -870,14 +860,14 @@
299 break
300
301 try:
302- self._sync_exec('/var/lib/zadara/bin/zadara_sncfg',
303- 'create_qospart',
304- '--qos', qosstr,
305- '--pname', volume['name'],
306- '--psize', sizestr,
307- '--vsaid', vsa_id,
308- run_as_root=True,
309- check_exit_code=0)
310+ self._execute('/var/lib/zadara/bin/zadara_sncfg',
311+ 'create_qospart',
312+ '--qos', qosstr,
313+ '--pname', volume['name'],
314+ '--psize', sizestr,
315+ '--vsaid', vsa_id,
316+ run_as_root=True,
317+ check_exit_code=0)
318 except exception.ProcessExecutionError:
319 LOG.debug(_("VSA BE create_volume for %s failed"), volume['name'])
320 raise
321@@ -895,11 +885,11 @@
322 return
323
324 try:
325- self._sync_exec('/var/lib/zadara/bin/zadara_sncfg',
326- 'delete_partition',
327- '--pname', volume['name'],
328- run_as_root=True,
329- check_exit_code=0)
330+ self._execute('/var/lib/zadara/bin/zadara_sncfg',
331+ 'delete_partition',
332+ '--pname', volume['name'],
333+ run_as_root=True,
334+ check_exit_code=0)
335 except exception.ProcessExecutionError:
336 LOG.debug(_("VSA BE delete_volume for %s failed"), volume['name'])
337 return
338@@ -980,12 +970,12 @@
339 return
340
341 try:
342- self._sync_exec('/var/lib/zadara/bin/zadara_sncfg',
343- 'remove_export',
344- '--pname', volume['name'],
345- '--tid', iscsi_target,
346- run_as_root=True,
347- check_exit_code=0)
348+ self._execute('/var/lib/zadara/bin/zadara_sncfg',
349+ 'remove_export',
350+ '--pname', volume['name'],
351+ '--tid', iscsi_target,
352+ run_as_root=True,
353+ check_exit_code=0)
354 except exception.ProcessExecutionError:
355 LOG.debug(_("VSA BE remove_export for %s failed"), volume['name'])
356 return
357@@ -1010,13 +1000,12 @@
358 Common logic that asks zadara_sncfg to setup iSCSI target/lun for
359 this volume
360 """
361- (out, err) = self._sync_exec(
362- '/var/lib/zadara/bin/zadara_sncfg',
363- 'create_export',
364- '--pname', volume['name'],
365- '--tid', iscsi_target,
366- run_as_root=True,
367- check_exit_code=0)
368+ (out, err) = self._execute('/var/lib/zadara/bin/zadara_sncfg',
369+ 'create_export',
370+ '--pname', volume['name'],
371+ '--tid', iscsi_target,
372+ run_as_root=True,
373+ check_exit_code=0)
374
375 result_xml = ElementTree.fromstring(out)
376 response_node = result_xml.find("Sn")
377@@ -1037,11 +1026,10 @@
378 def _get_qosgroup_summary(self):
379 """gets the list of qosgroups from Zadara BE"""
380 try:
381- (out, err) = self._sync_exec(
382- '/var/lib/zadara/bin/zadara_sncfg',
383- 'get_qosgroups_xml',
384- run_as_root=True,
385- check_exit_code=0)
386+ (out, err) = self._execute('/var/lib/zadara/bin/zadara_sncfg',
387+ 'get_qosgroups_xml',
388+ run_as_root=True,
389+ check_exit_code=0)
390 except exception.ProcessExecutionError:
391 LOG.debug(_("Failed to retrieve QoS info"))
392 return {}
393
394=== added file 'nova/volume/iscsi.py'
395--- nova/volume/iscsi.py 1970-01-01 00:00:00 +0000
396+++ nova/volume/iscsi.py 2011-09-18 15:48:23 +0000
397@@ -0,0 +1,156 @@
398+# vim: tabstop=4 shiftwidth=4 softtabstop=4
399+
400+# Copyright 2010 United States Government as represented by the
401+# Administrator of the National Aeronautics and Space Administration.
402+# All Rights Reserved.
403+#
404+# Licensed under the Apache License, Version 2.0 (the "License"); you may
405+# not use this file except in compliance with the License. You may obtain
406+# a copy of the License at
407+#
408+# http://www.apache.org/licenses/LICENSE-2.0
409+#
410+# Unless required by applicable law or agreed to in writing, software
411+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
412+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
413+# License for the specific language governing permissions and limitations
414+# under the License.
415+"""
416+Helper code for the iSCSI volume driver.
417+
418+"""
419+
420+from nova import flags
421+from nova import utils
422+
423+
424+FLAGS = flags.FLAGS
425+flags.DEFINE_string('iscsi_helper', 'ietadm',
426+ 'iscsi target user-land tool to use')
427+
428+
429+class TargetAdmin(object):
430+ """iSCSI target administration.
431+
432+ Base class for iSCSI target admin helpers.
433+ """
434+
435+ def __init__(self, cmd, execute):
436+ self._cmd = cmd
437+ self.set_execute(execute)
438+
439+ def set_execute(self, execute):
440+ """Set the function to be used to execute commands."""
441+ self._execute = execute
442+
443+ def _run(self, *args, **kwargs):
444+ self._execute(self._cmd, *args, run_as_root=True, **kwargs)
445+
446+ def new_target(self, name, tid, **kwargs):
447+ """Create a new iSCSI target."""
448+ raise NotImplementedError()
449+
450+ def delete_target(self, tid, **kwargs):
451+ """Delete a target."""
452+ raise NotImplementedError()
453+
454+ def show_target(self, tid, **kwargs):
455+ """Query the given target ID."""
456+ raise NotImplementedError()
457+
458+ def new_logicalunit(self, tid, lun, path, **kwargs):
459+ """Create a new LUN on a target using the supplied path."""
460+ raise NotImplementedError()
461+
462+ def delete_logicalunit(self, tid, lun, **kwargs):
463+ """Delete a logical unit from a target."""
464+ raise NotImplementedError()
465+
466+
467+class TgtAdm(TargetAdmin):
468+ """iSCSI target administration using tgtadm."""
469+
470+ def __init__(self, execute=utils.execute):
471+ super(TgtAdm, self).__init__('tgtadm', execute)
472+
473+ def new_target(self, name, tid, **kwargs):
474+ self._run('--op', 'new',
475+ '--lld=iscsi', '--mode=target',
476+ '--tid=%s' % tid,
477+ '--targetname=%s' % name,
478+ **kwargs)
479+ self._run('--op', 'bind',
480+ '--lld=iscsi', '--mode=target',
481+ '--initiator-address=ALL',
482+ '--tid=%s' % tid,
483+ **kwargs)
484+
485+ def delete_target(self, tid, **kwargs):
486+ self._run('--op', 'delete',
487+ '--lld=iscsi', '--mode=target',
488+ '--tid=%s' % tid,
489+ **kwargs)
490+
491+ def show_target(self, tid, **kwargs):
492+ self._run('--op', 'show',
493+ '--lld=iscsi', '--mode=target',
494+ '--tid=%s' % tid,
495+ **kwargs)
496+
497+ def new_logicalunit(self, tid, lun, path, **kwargs):
498+ self._run('--op', 'new',
499+ '--lld=iscsi', '--mode=logicalunit',
500+ '--tid=%s' % tid,
501+ '--lun=%d' % (lun + 1), # lun0 is reserved
502+ '--backing-store=%s' % path,
503+ **kwargs)
504+
505+ def delete_logicalunit(self, tid, lun, **kwargs):
506+ self._run('--op', 'delete',
507+ '--lld=iscsi', '--mode=logicalunit',
508+ '--tid=%s' % tid,
509+ '--lun=%d' % (lun + 1),
510+ **kwargs)
511+
512+
513+class IetAdm(TargetAdmin):
514+ """iSCSI target administration using ietadm."""
515+
516+ def __init__(self, execute=utils.execute):
517+ super(IetAdm, self).__init__('ietadm', execute)
518+
519+ def new_target(self, name, tid, **kwargs):
520+ self._run('--op', 'new',
521+ '--tid=%s' % tid,
522+ '--params', 'Name=%s' % name,
523+ **kwargs)
524+
525+ def delete_target(self, tid, **kwargs):
526+ self._run('--op', 'delete',
527+ '--tid=%s' % tid,
528+ **kwargs)
529+
530+ def show_target(self, tid, **kwargs):
531+ self._run('--op', 'show',
532+ '--tid=%s' % tid,
533+ **kwargs)
534+
535+ def new_logicalunit(self, tid, lun, path, **kwargs):
536+ self._run('--op', 'new',
537+ '--tid=%s' % tid,
538+ '--lun=%d' % lun,
539+ '--params', 'Path=%s,Type=fileio' % path,
540+ **kwargs)
541+
542+ def delete_logicalunit(self, tid, lun, **kwargs):
543+ self._run('--op', 'delete',
544+ '--tid=%s' % tid,
545+ '--lun=%d' % lun,
546+ **kwargs)
547+
548+
549+def get_target_admin():
550+ if FLAGS.iscsi_helper == 'tgtadm':
551+ return TgtAdm()
552+ else:
553+ return IetAdm()