Merge lp:~nacc/curtin/iscsi-wip into lp:~curtin-dev/curtin/trunk

Proposed by Nish Aravamudan
Status: Merged
Merged at revision: 469
Proposed branch: lp:~nacc/curtin/iscsi-wip
Merge into: lp:~curtin-dev/curtin/trunk
Diff against target: 2018 lines (+1700/-8)
20 files modified
curtin/block/iscsi.py (+402/-0)
curtin/commands/block_attach_iscsi.py (+38/-0)
curtin/commands/block_detach_iscsi.py (+36/-0)
curtin/commands/block_meta.py (+25/-5)
curtin/commands/curthooks.py (+18/-0)
curtin/commands/install.py (+3/-0)
curtin/commands/main.py (+2/-1)
curtin/deps/__init__.py (+1/-0)
curtin/util.py (+18/-2)
doc/topics/integration-testing.rst (+42/-0)
doc/topics/storage.rst (+48/-0)
examples/tests/basic_iscsi.yaml (+143/-0)
tests/unittests/test_block_iscsi.py (+435/-0)
tests/unittests/test_util.py (+25/-0)
tests/vmtests/__init__.py (+144/-0)
tests/vmtests/test_iscsi.py (+51/-0)
tools/find-tgt (+124/-0)
tools/jenkins-runner (+9/-0)
tools/launch (+135/-0)
tools/vmtest-system-setup (+1/-0)
To merge this branch: bzr merge lp:~nacc/curtin/iscsi-wip
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Blake Rouse Pending
Scott Moser Pending
Ryan Harper Pending
Review via email: mp+316783@code.launchpad.net

Commit message

Add iSCSI disk support.

iSCSI disks are specified following RFC4173 (https://tools.ietf.org/html/rfc4173) as:

  path: iscsi:[user:password[:initiatoruser:initiatorpassword]@]host:proto:port:lun:targetname

unittests for iSCSI target parsing have been added as well as a vmtests for testing iSCSI targets via tgt with all possible authentication combinations.

For standalone testing, tools/find-tgt has been added to spawn a tgt server as a regular user for serving iSCSI disks.

tools/jenkins-runner has been updated to use tools/find-tgt for the automated vmtests.

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
454. By Nish Aravamudan

tools/launch: fix apt_proxy support
tests/vmtests/__init__.py: use util.subp

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
455. By Nish Aravamudan

Drop control port (IPC socket is sufficient). Update jenkins-runner to
spawn tgt in order to test, if it can find IP.

456. By Nish Aravamudan

fix launch to use = for parameters to tgtadm
fix typo in __init__.py

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
457. By Nish Aravamudan

overexuberant, -I for some reason is a literal string (not a = value)

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
458. By Nish Aravamudan

Fix flake and tox errors.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
459. By Nish Aravamudan

Drop unnecessary + for string concatenation.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

This really great work. Thanks for pushing the MP; we know it's WIP but great to see the whole thing.

lp:~nacc/curtin/iscsi-wip updated
460. By Nish Aravamudan

Merge from ~smoser/iscsi-wip

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

lots of little things.

Revision history for this message
Scott Moser (smoser) wrote :

small chat with nacc in #curtin and i had more comments:
 - no real reason for the @properties in your class
 - unit tests on IscsiDisk would be good
 - a __str__ on the class would be good that easily prints the fully rendered string (possibly with "PASSWORD" for the password or something).

and i think that, if you write ipv6 with port, i think you have to specify [ff:00:...00]:port

surely in a : delimited rfc, that seems sane.

lp:~nacc/curtin/iscsi-wip updated
461. By Nish Aravamudan

First set of fixes for review comments.

462. By Nish Aravamudan

Fixes for prior commit and fix launch to actually skip iscsi disks.

463. By Nish Aravamudan

Remove apt proxy chagnes again

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
464. By Nish Aravamudan

fix launch further

465. By Nish Aravamudan

Fix tox

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
466. By Nish Aravamudan

merge with trunk

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
467. By Nish Aravamudan

Fixup ISCSI -> iSCSI.
Fix find-tgt to actually export a useful bash snippet (we need the info
variables to be exported to the environment or the vmtests will always
skip the iSCSI tests).

468. By Nish Aravamudan

remove stray unrelated change to launch
require iscsi: to start the path in case of an odd corner-case

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
469. By Nish Aravamudan

Fix block/iscsi capture subp parameter usage.
Fix tools/launch which dropped ACL on the smoser merge.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
470. By Nish Aravamudan

tools/launch: revert empty apt proxy support (unrelated to iscsi)

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

save_iscsi_config:
        LOG.info("fstab configuration is not present in environment, "
                 "so cannot locate an appropriate directory to write "
                 "iSCSI node file in so not writing iSCSI node file")

     ^^^ sounds like a warning at least, maybe a raise exception?

tests/vmtests/__init__.py:
  IPV4_PORTAL_REGEX, IPV6_PORTAL_REGEX
  I suggested the urlparse in part to be more robust in parsing.
  I quickly googled "valid ip6 address".
    http://www.ronnutter.com/ipv6-cheatsheet-on-identifying-valid-ipv6-addresses/

  Your regex misses several of those ("2001:db8:0:1")

  the urlparse would work on them.
  Then, since 3.3 (vmtest only has to run python3) you could use
    https://docs.python.org/3.3/library/ipaddress.html
  to validate it.

  I do fully realize this is just in a test class and input can be assumed
  to be friendly.

- # match target name to TID
  can you put an example there?

- still would like a __str__ in IscsiDisk (
- need unit tests.

lp:~nacc/curtin/iscsi-wip updated
471. By Nish Aravamudan

Review updates -- unittests, str representation of disks.

472. By Nish Aravamudan

Merge with trunk

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

Nish,
unit tests look really good, thank you for that.

lp:~nacc/curtin/iscsi-wip updated
473. By Nish Aravamudan

Cleanup unit tests.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
474. By Nish Aravamudan

Fix tox

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
475. By Nish Aravamudan

curtin/util::subp: add log_captured parameter

Defaults to false. If true, logs the captured output.

476. By Nish Aravamudan

iscsi: require portal parameter to iscsiadm_logout

477. By Nish Aravamudan

Allow hostnames in portal specification

Require IPv6 be specified in [] in both RFC4173 and portal.

478. By Nish Aravamudan

iscsi: add authentication support

479. By Nish Aravamudan

Add some extra debugging to find-tgt for running commands with C&P.

480. By Nish Aravamudan

Fix whitespace.

481. By Nish Aravamudan

Fix up logging output.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
482. By Nish Aravamudan

basic auth support for iSCSI (untested)

483. By Nish Aravamudan

Successful vmtest with auth.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
484. By Nish Aravamudan

iSCSI documentation

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
485. By Nish Aravamudan

Update docs for testing
Drop CURTIN_VMTEST_ISCSI_PORTAL_V{4,6} in favor of
CURTIN_VMTEST_ISCSI_PORTAL as we no longer distinguish between the two
formats at run-time.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
486. By Nish Aravamudan

Fix `make style-check`.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

We no longer have any callers of 'is_valid_ipv4_address' so its probably right to remove it.

lp:~nacc/curtin/iscsi-wip updated
487. By Nish Aravamudan

Address smoser's review comments.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
488. By Nish Aravamudan

Updates for review comments

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
489. By Nish Aravamudan

Do not assume /dev/disk/by-path exists (fixes vmtests)

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
490. By Nish Aravamudan

Ensure every vmtest has a _iscsi_disks member (defaults to an empty
list).

491. By Nish Aravamudan

Actually fix vmtests...

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
492. By Nish Aravamudan

Remove unused function.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
lp:~nacc/curtin/iscsi-wip updated
493. By Nish Aravamudan

vmtests: tgtadm in zesty rejects '_' in targetnames

494. By Nish Aravamudan

Empty _iscsi_disks on every iSCSI disk setup. Otherwise the variable
gets shared between instances of the class.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Nish Aravamudan (nacc) wrote :

Full vmtest run of r494: https://jenkins.ubuntu.com/server/view/Curtin/job/curtin-vmtest-debug/44/console should indicate success across the board except for two unrelated btrfs issues, which are present upstream.

lp:~nacc/curtin/iscsi-wip updated
495. By Nish Aravamudan

Merge with trunk

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Nish Aravamudan (nacc) wrote :

https://jenkins.ubuntu.com/server/view/Curtin/job/curtin-vmtest-debug/45/console

Passes all vmtests except for the known bcache change in the upstream kernel.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'curtin/block/iscsi.py'
2--- curtin/block/iscsi.py 1970-01-01 00:00:00 +0000
3+++ curtin/block/iscsi.py 2017-02-22 16:43:05 +0000
4@@ -0,0 +1,402 @@
5+# Copyright (C) 2017 Canonical Ltd.
6+#
7+# Author: Nishanth Aravamudan <nish.aravamudan@canonical.com>
8+#
9+# Curtin is free software: you can redistribute it and/or modify it under
10+# the terms of the GNU Affero General Public License as published by the
11+# Free Software Foundation, either version 3 of the License, or (at your
12+# option) any later version.
13+#
14+# Curtin is distributed in the hope that it will be useful, but WITHOUT ANY
15+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
17+# more details.
18+#
19+# You should have received a copy of the GNU Affero General Public License
20+# along with Curtin. If not, see <http://www.gnu.org/licenses/>.
21+
22+
23+# This module wraps calls to the iscsiadm utility for examining iSCSI
24+# devices. Functions prefixed with 'iscsiadm_' involve executing
25+# the 'iscsiadm' command in a subprocess. The remaining functions handle
26+# manipulation of the iscsiadm output.
27+
28+
29+import os
30+import re
31+import shutil
32+
33+from curtin import (util, udev)
34+from curtin.log import LOG
35+
36+_ISCSI_DISKS = {}
37+RFC4173_AUTH_REGEX = re.compile(r'''^
38+ (?P<user>[^:]*?):(?P<password>[^:]*?)
39+ (?::(?P<initiatoruser>[^:]*?):(?P<initiatorpassword>[^:]*?))?
40+ $
41+ ''', re.VERBOSE)
42+
43+RFC4173_TARGET_REGEX = re.compile(r'''^
44+ (?P<host>[^@]*): # greedy so ipv6 IPs are matched
45+ (?P<proto>[^:]*):
46+ (?P<port>[^:]*):
47+ (?P<lun>[^:]*):
48+ (?P<targetname>\S*) # greedy so entire suffix is matched
49+ $''', re.VERBOSE)
50+
51+ISCSI_PORTAL_REGEX = re.compile(r'^(?P<host>\S*):(?P<port>\d+)$')
52+
53+
54+# @portal is of the form: HOST:PORT
55+def assert_valid_iscsi_portal(portal):
56+ if not isinstance(portal, util.string_types):
57+ raise ValueError("iSCSI portal (%s) is not a string" % portal)
58+
59+ m = re.match(ISCSI_PORTAL_REGEX, portal)
60+ if m is None:
61+ raise ValueError("iSCSI portal (%s) is not in the format "
62+ "(HOST:PORT)" % portal)
63+
64+ host = m.group('host')
65+ if host.startswith('[') and host.endswith(']'):
66+ host = host[1:-1]
67+ if not util.is_valid_ipv6_address(host):
68+ raise ValueError("Invalid IPv6 address (%s) in iSCSI portal (%s)" %
69+ (host, portal))
70+
71+ try:
72+ port = int(m.group('port'))
73+ except ValueError:
74+ raise ValueError("iSCSI portal (%s) port (%s) is not an integer" %
75+ (portal, m.group('port')))
76+
77+ return host, port
78+
79+
80+def iscsiadm_sessions():
81+ cmd = ["iscsiadm", "--mode=session", "--op=show"]
82+ # rc 21 indicates no sessions currently exist, which is not
83+ # inherently incorrect (if not logged in yet)
84+ out, _ = util.subp(cmd, rcs=[0, 21], capture=True, log_captured=True)
85+ return out
86+
87+
88+def iscsiadm_discovery(portal):
89+ # only supported type for now
90+ type = 'sendtargets'
91+
92+ if not portal:
93+ raise ValueError("Portal must be specified for discovery")
94+
95+ cmd = ["iscsiadm", "--mode=discovery", "--type=%s" % type,
96+ "--portal=%s" % portal]
97+
98+ try:
99+ util.subp(cmd, capture=True, log_captured=True)
100+ except util.ProcessExecutionError as e:
101+ LOG.warning("iscsiadm_discovery to %s failed with exit code %d",
102+ portal, e.exit_code)
103+ raise
104+
105+
106+def iscsiadm_login(target, portal):
107+ LOG.debug('iscsiadm_login: target=%s portal=%s', target, portal)
108+
109+ cmd = ['iscsiadm', '--mode=node', '--targetname=%s' % target,
110+ '--portal=%s' % portal, '--login']
111+ util.subp(cmd, capture=True, log_captured=True)
112+
113+
114+def iscsiadm_set_automatic(target, portal):
115+ LOG.debug('iscsiadm_set_automatic: target=%s portal=%s', target, portal)
116+
117+ cmd = ['iscsiadm', '--mode=node', '--targetname=%s' % target,
118+ '--portal=%s' % portal, '--op=update',
119+ '--name=node.startup', '--value=automatic']
120+
121+ util.subp(cmd, capture=True, log_captured=True)
122+
123+
124+def iscsiadm_authenticate(target, portal, user=None, password=None,
125+ iuser=None, ipassword=None):
126+ LOG.debug('iscsiadm_authenticate: target=%s portal=%s '
127+ 'user=%s password=%s iuser=%s ipassword=%s',
128+ target, portal, user, "HIDDEN" if password else None,
129+ iuser, "HIDDEN" if ipassword else None)
130+
131+ if iuser or ipassword:
132+ cmd = ['iscsiadm', '--mode=node', '--targetname=%s' % target,
133+ '--portal=%s' % portal, '--op=update',
134+ '--name=node.session.auth.authmethod', '--value=CHAP']
135+ util.subp(cmd, capture=True, log_captured=True)
136+
137+ if iuser:
138+ cmd = ['iscsiadm', '--mode=node', '--targetname=%s' % target,
139+ '--portal=%s' % portal, '--op=update',
140+ '--name=node.session.auth.username_in',
141+ '--value=%s' % iuser]
142+ util.subp(cmd, capture=True, log_captured=True)
143+
144+ if ipassword:
145+ cmd = ['iscsiadm', '--mode=node', '--targetname=%s' % target,
146+ '--portal=%s' % portal, '--op=update',
147+ '--name=node.session.auth.password_in',
148+ '--value=%s' % ipassword]
149+ util.subp(cmd, capture=True, log_captured=True,
150+ logstring='iscsiadm --mode=node --targetname=%s '
151+ '--portal=%s --op=update '
152+ '--name=node.session.auth.password_in '
153+ '--value=HIDDEN' % (target, portal))
154+
155+ if user or password:
156+ cmd = ['iscsiadm', '--mode=node', '--targetname=%s' % target,
157+ '--portal=%s' % portal, '--op=update',
158+ '--name=node.session.auth.authmethod', '--value=CHAP']
159+ util.subp(cmd, capture=True, log_captured=True)
160+
161+ if user:
162+ cmd = ['iscsiadm', '--mode=node', '--targetname=%s' % target,
163+ '--portal=%s' % portal, '--op=update',
164+ '--name=node.session.auth.username',
165+ '--value=%s' % user]
166+ util.subp(cmd, capture=True, log_captured=True)
167+
168+ if password:
169+ cmd = ['iscsiadm', '--mode=node', '--targetname=%s' % target,
170+ '--portal=%s' % portal, '--op=update',
171+ '--name=node.session.auth.password',
172+ '--value=%s' % password]
173+ util.subp(cmd, capture=True, log_captured=True,
174+ logstring='iscsiadm --mode=node --targetname=%s '
175+ '--portal=%s --op=update '
176+ '--name=node.session.auth.password '
177+ '--value=HIDDEN' % (target, portal))
178+
179+
180+def iscsiadm_logout(target, portal):
181+ LOG.debug('iscsiadm_logout: target=%s portal=%s', target, portal)
182+
183+ cmd = ['iscsiadm', '--mode=node', '--targetname=%s' % target,
184+ '--portal=%s' % portal, '--logout']
185+ util.subp(cmd, capture=True, log_captured=True)
186+
187+ udev.udevadm_settle()
188+
189+
190+def target_nodes_directory(state, iscsi_disk):
191+ # we just want to copy in the nodes portion
192+ target_nodes_location = os.path.dirname(
193+ os.path.join(os.path.split(state['fstab'])[0],
194+ iscsi_disk.etciscsi_nodefile[len('/etc/iscsi/'):]))
195+ os.makedirs(target_nodes_location)
196+ return target_nodes_location
197+
198+
199+def save_iscsi_config(iscsi_disk):
200+ state = util.load_command_environment()
201+ # A nodes directory will be created in the same directory as the
202+ # fstab in the configuration. This will then be copied onto the
203+ # system later
204+ if state['fstab']:
205+ target_nodes_location = target_nodes_directory(state, iscsi_disk)
206+ shutil.copy(iscsi_disk.etciscsi_nodefile, target_nodes_location)
207+ else:
208+ LOG.info("fstab configuration is not present in environment, "
209+ "so cannot locate an appropriate directory to write "
210+ "iSCSI node file in so not writing iSCSI node file")
211+
212+
213+def ensure_disk_connected(rfc4173, write_config=True):
214+ global _ISCSI_DISKS
215+ iscsi_disk = _ISCSI_DISKS.get(rfc4173)
216+ if not iscsi_disk:
217+ iscsi_disk = IscsiDisk(rfc4173)
218+ try:
219+ iscsi_disk.connect()
220+ except util.ProcessExecutionError:
221+ LOG.error('Unable to connect to iSCSI disk (%s)' % rfc4173)
222+ # what should we do in this case?
223+ raise
224+ if write_config:
225+ save_iscsi_config(iscsi_disk)
226+ _ISCSI_DISKS.update({rfc4173: iscsi_disk})
227+
228+ # this is just a sanity check that the disk is actually present and
229+ # the above did what we expected
230+ if not os.path.exists(iscsi_disk.devdisk_path):
231+ LOG.warn('Unable to find iSCSI disk for target (%s) by path (%s)',
232+ iscsi_disk.target, iscsi_disk.devdisk_path)
233+
234+ return iscsi_disk
235+
236+
237+def connected_disks():
238+ global _ISCSI_DISKS
239+ return _ISCSI_DISKS
240+
241+
242+def disconnect_target_disks(target_root_path=None):
243+ target_nodes_path = util.target_path(target_root_path, '/etc/iscsi/nodes')
244+ fails = []
245+ if os.path.isdir(target_nodes_path):
246+ for target in os.listdir(target_nodes_path):
247+ # conn is "host,port,lun"
248+ for conn in os.listdir(
249+ os.path.sep.join([target_nodes_path, target])):
250+ host, port, _ = conn.split(',')
251+ try:
252+ util.subp(['sync'])
253+ iscsiadm_logout(target, '%s:%s' % (host, port))
254+ except util.ProcessExecutionError as e:
255+ fails.append(target)
256+ LOG.warn("Unable to logout of iSCSI target %s: %s",
257+ target, e)
258+
259+ if fails:
260+ raise RuntimeError(
261+ "Unable to logout of iSCSI targets: %s" % ', '.join(fails))
262+
263+
264+# Determines if a /dev/disk/by-path symlink matching the udev pattern
265+# for iSCSI disks is pointing at @kname
266+def kname_is_iscsi(kname):
267+ by_path = "/dev/disk/by-path"
268+ if os.path.isdir(by_path):
269+ for path in os.listdir(by_path):
270+ path_target = os.path.realpath(os.path.sep.join([by_path, path]))
271+ if kname in path_target and 'iscsi' in path:
272+ LOG.debug('kname_is_iscsi: '
273+ 'found by-path link %s for kname %s', path, kname)
274+ return True
275+ LOG.debug('kname_is_iscsi: no iscsi disk found for kname %s' % kname)
276+ return False
277+
278+
279+class IscsiDisk(object):
280+ # Per Debian bug 804162, the iscsi specifier looks like
281+ # TARGETSPEC=host:proto:port:lun:targetname
282+ # root=iscsi:$TARGETSPEC
283+ # root=iscsi:user:password@$TARGETSPEC
284+ # root=iscsi:user:password:initiatoruser:initiatorpassword@$TARGETSPEC
285+ def __init__(self, rfc4173):
286+ auth_m = None
287+ _rfc4173 = rfc4173
288+ if not rfc4173.startswith('iscsi:'):
289+ raise ValueError('iSCSI specification (%s) did not start with '
290+ 'iscsi:. iSCSI disks must be specified as '
291+ 'iscsi:[user:password[:initiatoruser:'
292+ 'initiatorpassword]@]'
293+ 'host:proto:port:lun:targetname' % _rfc4173)
294+ rfc4173 = rfc4173[6:]
295+ if '@' in rfc4173:
296+ if rfc4173.count('@') != 1:
297+ raise ValueError('Only one @ symbol allowed in iSCSI disk '
298+ 'specification (%s). iSCSI disks must be '
299+ 'specified as'
300+ 'iscsi:[user:password[:initiatoruser:'
301+ 'initiatorpassword]@]'
302+ 'host:proto:port:lun:targetname' % _rfc4173)
303+ auth, target = rfc4173.split('@')
304+ auth_m = RFC4173_AUTH_REGEX.match(auth)
305+ if auth_m is None:
306+ raise ValueError('Invalid authentication specified for iSCSI '
307+ 'disk (%s). iSCSI disks must be specified as '
308+ 'iscsi:[user:password[:initiatoruser:'
309+ 'initiatorpassword]@]'
310+ 'host:proto:port:lun:targetname' % _rfc4173)
311+ else:
312+ target = rfc4173
313+
314+ target_m = RFC4173_TARGET_REGEX.match(target)
315+ if target_m is None:
316+ raise ValueError('Invalid target specified for iSCSI disk (%s). '
317+ 'iSCSI disks must be specified as '
318+ 'iscsi:[user:password[:initiatoruser:'
319+ 'initiatorpassword]@]'
320+ 'host:proto:port:lun:targetname' % _rfc4173)
321+
322+ if target_m.group('proto') and target_m.group('proto') != '6':
323+ LOG.warn('Specified protocol for iSCSI (%s) is unsupported, '
324+ 'assuming 6 (TCP)', target_m.group('proto'))
325+
326+ if not target_m.group('host') or not target_m.group('targetname'):
327+ raise ValueError('Both host and targetname must be specified for '
328+ 'iSCSI disks')
329+
330+ if auth_m:
331+ self.user = auth_m.group('user')
332+ self.password = auth_m.group('password')
333+ self.iuser = auth_m.group('initiatoruser')
334+ self.ipassword = auth_m.group('initiatorpassword')
335+ else:
336+ self.user = None
337+ self.password = None
338+ self.iuser = None
339+ self.ipassword = None
340+
341+ self.host = target_m.group('host')
342+ self.proto = '6'
343+ self.lun = int(target_m.group('lun')) if target_m.group('lun') else 0
344+ self.target = target_m.group('targetname')
345+
346+ try:
347+ self.port = int(target_m.group('port')) if target_m.group('port') \
348+ else 3260
349+
350+ except ValueError:
351+ raise ValueError('Specified iSCSI port (%s) is not an integer' %
352+ target_m.group('port'))
353+
354+ portal = '%s:%s' % (self.host, self.port)
355+ if self.host.startswith('[') and self.host.endswith(']'):
356+ self.host = self.host[1:-1]
357+ if not util.is_valid_ipv6_address(self.host):
358+ raise ValueError('Specified iSCSI IPv6 address (%s) is not '
359+ 'valid' % self.host)
360+ portal = '[%s]:%s' % (self.host, self.port)
361+ assert_valid_iscsi_portal(portal)
362+ self.portal = portal
363+
364+ def __str__(self):
365+ rep = 'iscsi'
366+ if self.user:
367+ rep += ':%s:PASSWORD' % self.user
368+ if self.iuser:
369+ rep += ':%s:IPASSWORD' % self.iuser
370+ rep += ':%s:%s:%s:%s:%s' % (self.host, self.proto, self.port,
371+ self.lun, self.target)
372+ return rep
373+
374+ @property
375+ def etciscsi_nodefile(self):
376+ return '/etc/iscsi/nodes/%s/%s,%s,%s/default' % (
377+ self.target, self.host, self.port, self.lun)
378+
379+ @property
380+ def devdisk_path(self):
381+ return '/dev/disk/by-path/ip-%s-iscsi-%s-lun-%s' % (
382+ self.portal, self.target, self.lun)
383+
384+ def connect(self):
385+ if self.target in iscsiadm_sessions():
386+ return
387+
388+ iscsiadm_discovery(self.portal)
389+
390+ iscsiadm_authenticate(self.target, self.portal, self.user,
391+ self.password, self.iuser, self.ipassword)
392+
393+ iscsiadm_login(self.target, self.portal)
394+
395+ udev.udevadm_settle(self.devdisk_path)
396+
397+ iscsiadm_set_automatic(self.target, self.portal)
398+
399+ def disconnect(self):
400+ if self.target not in iscsiadm_sessions():
401+ return
402+
403+ util.subp(['sync'])
404+ iscsiadm_logout(self.target, self.portal)
405+
406+# vi: ts=4 expandtab syntax=python
407
408=== added file 'curtin/commands/block_attach_iscsi.py'
409--- curtin/commands/block_attach_iscsi.py 1970-01-01 00:00:00 +0000
410+++ curtin/commands/block_attach_iscsi.py 2017-02-22 16:43:05 +0000
411@@ -0,0 +1,38 @@
412+# Copyright (C) 2017 Canonical Ltd.
413+#
414+# Author: Nishanth Aravamudan <nish.aravamudan@canonical.com>
415+#
416+# Curtin is free software: you can redistribute it and/or modify it under
417+# the terms of the GNU Affero General Public License as published by the
418+# Free Software Foundation, either version 3 of the License, or (at your
419+# option) any later version.
420+#
421+# Curtin is distributed in the hope that it will be useful, but WITHOUT ANY
422+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
423+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
424+# more details.
425+#
426+# You should have received a copy of the GNU Affero General Public License
427+# along with Curtin. If not, see <http://www.gnu.org/licenses/>.
428+
429+from . import populate_one_subcmd
430+from curtin.block import iscsi
431+
432+
433+def block_attach_iscsi_main(args):
434+ iscsi.ensure_disk_connected(args.disk, args.save_config)
435+
436+ return 0
437+
438+
439+CMD_ARGUMENTS = (
440+ ('disk',
441+ {'help': 'RFC4173 specification of iSCSI disk to attach'}),
442+ ('--save-config',
443+ {'help': 'save access configuration to local filesystem',
444+ 'default': False, 'action': 'store_true'}),
445+)
446+
447+
448+def POPULATE_SUBCMD(parser):
449+ populate_one_subcmd(parser, CMD_ARGUMENTS, block_attach_iscsi_main)
450
451=== added file 'curtin/commands/block_detach_iscsi.py'
452--- curtin/commands/block_detach_iscsi.py 1970-01-01 00:00:00 +0000
453+++ curtin/commands/block_detach_iscsi.py 2017-02-22 16:43:05 +0000
454@@ -0,0 +1,36 @@
455+# Copyright (C) 2017 Canonical Ltd.
456+#
457+# Author: Nishanth Aravamudan <nish.aravamudan@canonical.com>
458+#
459+# Curtin is free software: you can redistribute it and/or modify it under
460+# the terms of the GNU Affero General Public License as published by the
461+# Free Software Foundation, either version 3 of the License, or (at your
462+# option) any later version.
463+#
464+# Curtin is distributed in the hope that it will be useful, but WITHOUT ANY
465+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
466+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
467+# more details.
468+#
469+# You should have received a copy of the GNU Affero General Public License
470+# along with Curtin. If not, see <http://www.gnu.org/licenses/>.
471+
472+from . import populate_one_subcmd
473+from curtin.block import iscsi
474+
475+
476+def block_detach_iscsi_main(args):
477+ i = iscsi.IscsiDisk(args.disk)
478+ i.disconnect()
479+
480+ return 0
481+
482+
483+CMD_ARGUMENTS = (
484+ ('disk',
485+ {'help': 'RFC4173 specification of iSCSI disk to attach'}),
486+)
487+
488+
489+def POPULATE_SUBCMD(parser):
490+ populate_one_subcmd(parser, CMD_ARGUMENTS, block_detach_iscsi_main)
491
492=== modified file 'curtin/commands/block_meta.py'
493--- curtin/commands/block_meta.py 2017-02-09 16:09:44 +0000
494+++ curtin/commands/block_meta.py 2017-02-22 16:43:05 +0000
495@@ -17,7 +17,7 @@
496
497 from collections import OrderedDict
498 from curtin import (block, config, util)
499-from curtin.block import (mdadm, mkfs, clear_holders, lvm)
500+from curtin.block import (mdadm, mkfs, clear_holders, lvm, iscsi)
501 from curtin.log import LOG
502 from curtin.reporter import events
503
504@@ -273,9 +273,14 @@
505 if vol.get('serial'):
506 volume_path = block.lookup_disk(vol.get('serial'))
507 elif vol.get('path'):
508- # resolve any symlinks to the dev_kname so sys/class/block access
509- # is valid. ie, there are no udev generated values in sysfs
510- volume_path = os.path.realpath(vol.get('path'))
511+ if vol.get('path').startswith('iscsi:'):
512+ i = iscsi.ensure_disk_connected(vol.get('path'))
513+ volume_path = os.path.realpath(i.devdisk_path)
514+ else:
515+ # resolve any symlinks to the dev_kname so
516+ # sys/class/block access is valid. ie, there are no
517+ # udev generated values in sysfs
518+ volume_path = os.path.realpath(vol.get('path'))
519 elif vol.get('wwn'):
520 by_wwn = '/dev/disk/by-id/wwn-%s' % vol.get('wwn')
521 volume_path = os.path.realpath(by_wwn)
522@@ -629,7 +634,22 @@
523 options = "sw"
524 else:
525 path = "/%s" % path
526- options = "defaults"
527+ if volume.get('type') == "partition":
528+ disk_block_path = get_path_to_storage_volume(
529+ volume.get('device'), storage_config)
530+ disk_kname = block.path_to_kname(disk_block_path)
531+ if iscsi.kname_is_iscsi(disk_kname):
532+ options = "_netdev"
533+ else:
534+ options = "defaults"
535+ elif volume.get('type') == "disk":
536+ disk_kname = block.path_to_kname(location)
537+ if iscsi.kname_is_iscsi(disk_kname):
538+ options = "_netdev"
539+ else:
540+ options = "defaults"
541+ else:
542+ options = "defaults"
543
544 if filesystem.get('fstype') in ["fat", "fat12", "fat16", "fat32",
545 "fat64"]:
546
547=== modified file 'curtin/commands/curthooks.py'
548--- curtin/commands/curthooks.py 2017-02-10 20:53:57 +0000
549+++ curtin/commands/curthooks.py 2017-02-22 16:43:05 +0000
550@@ -395,6 +395,16 @@
551 shutil.copy(crypttab, os.path.sep.join([target, 'etc/crypttab']))
552
553
554+def copy_iscsi_conf(nodes_dir, target):
555+ if not nodes_dir:
556+ LOG.warn("nodes directory must be specified, not copying")
557+ return
558+
559+ LOG.info("copying iscsi nodes database into target")
560+ shutil.copytree(nodes_dir, os.path.sep.join([target,
561+ 'etc/iscsi/nodes']))
562+
563+
564 def copy_mdadm_conf(mdadm_conf, target):
565 if not mdadm_conf:
566 LOG.warn("mdadm config must be specified, not copying")
567@@ -694,6 +704,14 @@
568 # packages may be needed prior to installing kernel
569 install_missing_packages(cfg, target)
570
571+ # If a /etc/iscsi/nodes/... file was created by block_meta then it
572+ # needs to be copied onto the target system
573+ nodes_location = os.path.join(os.path.split(state['fstab'])[0],
574+ "nodes")
575+ if os.path.exists(nodes_location):
576+ copy_iscsi_conf(nodes_location, target)
577+ # do we need to reconfigure open-iscsi?
578+
579 # If a mdadm.conf file was created by block_meta than it needs to be copied
580 # onto the target system
581 mdadm_location = os.path.join(os.path.split(state['fstab'])[0],
582
583=== modified file 'curtin/commands/install.py'
584--- curtin/commands/install.py 2017-02-10 20:53:57 +0000
585+++ curtin/commands/install.py 2017-02-22 16:43:05 +0000
586@@ -26,6 +26,7 @@
587 import tempfile
588
589 from curtin import block
590+from curtin.block import iscsi
591 from curtin import config
592 from curtin import util
593 from curtin import version
594@@ -452,6 +453,8 @@
595 copy_install_log(logfile, workingd.target, log_target_path)
596 for d in ('sys', 'dev', 'proc'):
597 util.do_umount(os.path.join(workingd.target, d))
598+ # need to do some processing on iscsi disks to disconnect?
599+ iscsi.disconnect_target_disks(workingd.target)
600 mounted = block.get_mountpoints()
601 mounted.sort(key=lambda x: -1 * x.count("/"))
602 for d in filter(lambda x: workingd.target in x, mounted):
603
604=== modified file 'curtin/commands/main.py'
605--- curtin/commands/main.py 2017-02-07 17:22:27 +0000
606+++ curtin/commands/main.py 2017-02-22 16:43:05 +0000
607@@ -29,7 +29,8 @@
608 VERSIONSTR = version.version_string()
609
610 SUB_COMMAND_MODULES = [
611- 'apply_net', 'block-info', 'block-meta', 'block-wipe', 'curthooks',
612+ 'apply_net', 'block-attach-iscsi', 'block-detach-iscsi',
613+ 'block-info', 'block-meta', 'block-wipe', 'curthooks',
614 'clear-holders', 'extract', 'hook', 'in-target', 'install', 'mkfs',
615 'net-meta', 'apt-config', 'pack', 'swap', 'system-install',
616 'system-upgrade', 'version']
617
618=== modified file 'curtin/deps/__init__.py'
619--- curtin/deps/__init__.py 2017-02-09 19:59:11 +0000
620+++ curtin/deps/__init__.py 2017-02-22 16:43:05 +0000
621@@ -44,6 +44,7 @@
622 ('sgdisk', 'gdisk'),
623 ('udevadm', 'udev'),
624 ('make-bcache', 'bcache-tools'),
625+ ('iscsiadm', 'open-iscsi'),
626 ]
627
628 if lsb_release()['codename'] == "precise":
629
630=== modified file 'curtin/util.py'
631--- curtin/util.py 2017-02-10 20:53:57 +0000
632+++ curtin/util.py 2017-02-22 16:43:05 +0000
633@@ -64,8 +64,9 @@
634 BASIC_MATCHER = re.compile(r'\$\{([A-Za-z0-9_.]+)\}|\$([A-Za-z0-9_.]+)')
635
636
637-def _subp(args, data=None, rcs=None, env=None, capture=False, shell=False,
638- logstring=False, decode="replace", target=None, cwd=None):
639+def _subp(args, data=None, rcs=None, env=None, capture=False,
640+ shell=False, logstring=False, decode="replace",
641+ target=None, cwd=None, log_captured=False):
642 if rcs is None:
643 rcs = [0]
644
645@@ -116,6 +117,9 @@
646 if devnull_fp:
647 devnull_fp.close()
648
649+ if capture and log_captured:
650+ LOG.debug("Command returned stdout=%s, stderr=%s", out, err)
651+
652 rc = sp.returncode # pylint: disable=E1101
653 if rc not in rcs:
654 raise ProcessExecutionError(stdout=out, stderr=err,
655@@ -137,6 +141,10 @@
656 :param capture:
657 boolean indicating if output should be captured. If True, then stderr
658 and stdout will be returned. If False, they will not be redirected.
659+ :param log_captured:
660+ boolean indicating if output should be logged on capture. If
661+ True, then stderr and stdout will be logged at DEBUG level. If
662+ False, they will not be logged.
663 :param shell: boolean indicating if this should be run with a shell.
664 :param logstring:
665 the command will be logged to DEBUG. If it contains info that should
666@@ -1107,6 +1115,14 @@
667 return False
668
669
670+def is_valid_ipv6_address(addr):
671+ try:
672+ socket.inet_pton(socket.AF_INET6, addr)
673+ except socket.error:
674+ return False
675+ return True
676+
677+
678 def is_resolvable_url(url):
679 """determine if this url is resolvable (existing or ip)."""
680 return is_resolvable(urlparse(url).hostname)
681
682=== modified file 'doc/topics/integration-testing.rst'
683--- doc/topics/integration-testing.rst 2017-02-06 20:29:33 +0000
684+++ doc/topics/integration-testing.rst 2017-02-22 16:43:05 +0000
685@@ -103,6 +103,10 @@
686 Running tests is done most simply by::
687
688 make vmtest
689+.. note::
690+
691+ By default, the vmtests for iSCSI will be skipped (see Environment
692+ Variable section for details).
693
694 If you wish to all tests in test_network.py, do so with::
695
696@@ -233,6 +237,40 @@
697 late_commands:
698 02_something: ['sh', '-xc', 'curtin in-target -- <yourcommand>']
699
700+- ``CURTIN_VMTEST_ISCSI_PORTAL``: default ''
701+
702+ By default, iSCSI tests are skipped when running `make vmtest`, as
703+ iSCSI server configuration is necessary. ``tools/jenkins-runner`` will
704+ configure a ``tgt`` server if possible and set the necessary
705+ environment variables.
706+
707+ If an accessible iSCSI server is available, it can be specified in
708+ this environment variable as ``HOST:PORT``. ``HOST`` can be a
709+ hostname, IPv4 address or IPv6 address. If an IPv6 address is used, it
710+ must be enclosed in ``[]``.
711+
712+ Additionally, if a ``tgt`` server is running locally as the iSCSI
713+ server and is configured to listen on a non-default socket, it is
714+ necessary to specify ``TGT_IPC_SOCKET`` to indicate the path to the
715+ socket in use.
716+
717+ As iSCSI server configuration by-hand can be difficult, there is a
718+ script in ``tools/find-tgt`` which can be used to run a local ``tgt``
719+ server. It will find an available port and use the default route-able
720+ IPv4 address on the system. The script takes a directory as parameter,
721+ and will emit a ``info`` file in that directory which can be sourced as
722+ a shell script to set the relevant environment variables needed to run
723+ the iSCSI vmtests. For example::
724+
725+ mkdir output
726+ ./tools/find-tgt output
727+ . output/info
728+ nosetests3 tests/vmtests/test_iscsi.py
729+
730+ Or, using ``jenkins-runner``:
731+
732+ ./tools/jenkins-runner tests/vmtests/test_iscsi.py
733+
734 Environment 'boolean' values
735 ============================
736
737@@ -281,3 +319,7 @@
738 - ``disk_driver``:
739
740 Default block device driver is ``virtio-blk``.
741+
742+iSCSI Setup:
743+
744+- ``iscsi_disks``:
745
746=== modified file 'doc/topics/storage.rst'
747--- doc/topics/storage.rst 2016-06-01 15:38:22 +0000
748+++ doc/topics/storage.rst 2017-02-22 16:43:05 +0000
749@@ -77,6 +77,48 @@
750 ``path`` are specified, curtin will use the serial number and ignore the path
751 that was specified.
752
753+iSCSI disks are supported via a special path prefix of 'iscsi:'. If this
754+prefix is found in the path specification for a disk, it is assumed to
755+be an iSCSI disk specification and must be in a `RFC4173
756+<https://tools.ietf.org/html/rfc4173>`_ compliant format, with
757+extensions from Debian for supporting authentication:
758+
759+``iscsi:[user:password[:iuser:ipassword]@]host:proto:port:lun:targetname``
760+
761+- ``user``: User to authenticate with, if needed, for iSCSI initiator
762+ authentication. Only CHAP authentication is supported at this time.
763+- ``password``: Password to authenticate with, if needed, for iSCSI
764+ initiator authentication. Only CHAP authentication is supported at
765+ this time.
766+- ``iuser``: User to authenticate with, if needed, for iSCSI target
767+ authentication. Only CHAP authentication is supported at this time.
768+- ``ipassword``: Password to authenticate with, if needed, for iSCSI
769+ target authentication. Only CHAP authentication is supported at this
770+ time.
771+.. note::
772+
773+ Curtin will treat it as an error if the user and password are not both
774+ specified for initiator and target authentication.
775+- ``host``: iSCSI server hosting the specified target. It can be a
776+ hostname, IPv4 or IPv6 address. If specified as an IPv6 address, it
777+ must be specified as ``[address]``.
778+- ``proto``: Specifies the protocol used for iSCSI. Currently only
779+ ``6``, or TCP, is supported and any other value is ignored. If not
780+ specified, ``6`` is assumed.
781+- ``port``: Specifies the port the iSCSI server is listening on. If not
782+ specified, ``3260`` is assumed.
783+- ``lun``: Specifies the LUN of the iSCSI target to connect to. If not
784+ specified, ``0`` is assumed.
785+- ``targetname``: Specifies the iSCSI target to connect to, by its name
786+ on the iSCSI server.
787+.. note::
788+
789+ Curtin will treat it as an error if the host and targetname are not
790+ specified.
791+
792+Any iSCSI disks specified will be configured to login at boot in the
793+target.
794+
795 **model**: *<disk model>*
796
797 This can specify the manufacturer or model of the disk. It is not currently
798@@ -313,6 +355,12 @@
799 config. The target device must already contain a valid filesystem and be
800 accessible.
801
802+.. note::
803+
804+ If the specified device refers to an iSCSI device, the corresponding
805+ fstab entry will contain ``_netdev`` to indicate networking is
806+ required to mount this filesystem.
807+
808 **Config Example**::
809
810 - id: disk0-part1-fs1-mount0
811
812=== added file 'examples/tests/basic_iscsi.yaml'
813--- examples/tests/basic_iscsi.yaml 1970-01-01 00:00:00 +0000
814+++ examples/tests/basic_iscsi.yaml 2017-02-22 16:43:05 +0000
815@@ -0,0 +1,143 @@
816+storage:
817+ version: 1
818+ config:
819+ - id: vdb
820+ type: disk
821+ ptable: msdos
822+ model: QEMU HARDDISK
823+ path: /dev/vdb
824+ name: main_disk
825+ wipe: superblock
826+ grub_device: true
827+ - id: vdb1
828+ type: partition
829+ number: 1
830+ size: 3GB
831+ device: vdb
832+ flag: boot
833+ - id: vdb2
834+ type: partition
835+ number: 2
836+ size: 1GB
837+ device: vdb
838+ - id: vdb1_root
839+ type: format
840+ fstype: ext4
841+ volume: vdb1
842+ - id: vdb2_home
843+ type: format
844+ fstype: ext4
845+ volume: vdb2
846+ - id: vdb1_mount
847+ type: mount
848+ path: /
849+ device: vdb1_root
850+ - id: vdb2_mount
851+ type: mount
852+ path: /home
853+ device: vdb2_home
854+ - id: sda
855+ type: disk
856+ path: iscsi:__RFC4173__
857+ name: iscsi_disk1
858+ ptable: msdos
859+ wipe: superblock
860+ - id: sda-part1
861+ type: partition
862+ number: 1
863+ size: 2GB
864+ device: sda
865+ - id: sda-part1-fs1
866+ type: format
867+ fstype: ext4
868+ label: cloud-image1
869+ volume: sda-part1
870+ - id: sda-part1-fs1-mount0
871+ type: mount
872+ path: /mnt/iscsi1
873+ device: sda-part1-fs1
874+ - id: sdb
875+ type: disk
876+ path: iscsi:__RFC4173__
877+ name: iscsi_disk2
878+ ptable: msdos
879+ wipe: superblock
880+ - id: sdb-part1
881+ type: partition
882+ number: 1
883+ size: 3GB
884+ device: sdb
885+ - id: sdb-part1-fs1
886+ type: format
887+ fstype: ext4
888+ label: cloud-image2
889+ volume: sdb-part1
890+ - id: sdb-part1-fs1-mount0
891+ type: mount
892+ path: /mnt/iscsi2
893+ device: sdb-part1-fs1
894+ - id: sdc
895+ type: disk
896+ path: iscsi:__RFC4173__
897+ name: iscsi_disk3
898+ ptable: msdos
899+ wipe: superblock
900+ - id: sdc-part1
901+ type: partition
902+ number: 1
903+ size: 4GB
904+ device: sdc
905+ - id: sdc-part1-fs1
906+ type: format
907+ fstype: ext4
908+ label: cloud-image3
909+ volume: sdc-part1
910+ - id: sdc-part1-fs1-mount0
911+ type: mount
912+ path: /mnt/iscsi3
913+ device: sdc-part1-fs1
914+ - id: sdd
915+ type: disk
916+ path: iscsi:__RFC4173__
917+ name: iscsi_disk4
918+ ptable: msdos
919+ wipe: superblock
920+ - id: sdd-part1
921+ type: partition
922+ number: 1
923+ size: 5GB
924+ device: sdd
925+ - id: sdd-part1-fs1
926+ type: format
927+ fstype: ext4
928+ label: cloud-image4
929+ volume: sdd-part1
930+ - id: sdd-part1-fs1-mount0
931+ type: mount
932+ path: /mnt/iscsi4
933+ device: sdd-part1-fs1
934+network:
935+ version: 1
936+ config:
937+ - type: physical
938+ name: interface0
939+ mac_address: "52:54:00:12:34:00"
940+ subnets:
941+ - type: dhcp
942+write_files:
943+ f1:
944+ path: /mnt/iscsi1/testfile
945+ content: "test1"
946+ permissions: 0777
947+ f2:
948+ path: /mnt/iscsi2/testfile
949+ content: "test2"
950+ permissions: 0777
951+ f3:
952+ path: /mnt/iscsi3/testfile
953+ content: "test3"
954+ permissions: 0777
955+ f4:
956+ path: /mnt/iscsi4/testfile
957+ content: "test4"
958+ permissions: 0777
959
960=== added file 'tests/unittests/test_block_iscsi.py'
961--- tests/unittests/test_block_iscsi.py 1970-01-01 00:00:00 +0000
962+++ tests/unittests/test_block_iscsi.py 2017-02-22 16:43:05 +0000
963@@ -0,0 +1,435 @@
964+from unittest import TestCase
965+from curtin.block import iscsi
966+
967+
968+class TestBlockIscsiPortalParsing(TestCase):
969+ def test_iscsi_portal_parsing_string(self):
970+ with self.assertRaisesRegexp(ValueError, 'not a string'):
971+ iscsi.assert_valid_iscsi_portal(1234)
972+
973+ def test_iscsi_portal_parsing_no_port(self):
974+ # port must be specified
975+ with self.assertRaisesRegexp(ValueError, 'not in the format'):
976+ iscsi.assert_valid_iscsi_portal('192.168.1.12')
977+ with self.assertRaisesRegexp(ValueError, 'not in the format'):
978+ iscsi.assert_valid_iscsi_portal('fe80::a634:d9ff:fe40:768a')
979+ with self.assertRaisesRegexp(ValueError, 'not in the format'):
980+ iscsi.assert_valid_iscsi_portal('192.168.1.12:')
981+ with self.assertRaisesRegexp(ValueError, 'not in the format'):
982+ iscsi.assert_valid_iscsi_portal('test.example.com:')
983+
984+ def test_iscsi_portal_parsing_valid_ip(self):
985+ # IP must be in [] for IPv6, if not we misparse
986+ host, port = iscsi.assert_valid_iscsi_portal(
987+ 'fe80::a634:d9ff:fe40:768a:9999')
988+ self.assertEquals(host, 'fe80::a634:d9ff:fe40:768a')
989+ self.assertEquals(port, 9999)
990+ # IP must not be in [] if port is specified for IPv4
991+ with self.assertRaisesRegexp(ValueError, 'Invalid IPv6 address'):
992+ iscsi.assert_valid_iscsi_portal('[192.168.1.12]:9000')
993+ with self.assertRaisesRegexp(ValueError, 'Invalid IPv6 address'):
994+ iscsi.assert_valid_iscsi_portal('[test.example.com]:8000')
995+
996+ def test_iscsi_portal_parsing_ip(self):
997+ with self.assertRaisesRegexp(ValueError, 'Invalid IPv6 address'):
998+ iscsi.assert_valid_iscsi_portal(
999+ '[1200::AB00:1234::2552:7777:1313]:9999')
1000+ # cannot distinguish between bad IP and bad hostname
1001+ host, port = iscsi.assert_valid_iscsi_portal('192.168:9000')
1002+ self.assertEquals(host, '192.168')
1003+ self.assertEquals(port, 9000)
1004+
1005+ def test_iscsi_portal_parsing_port(self):
1006+ with self.assertRaisesRegexp(ValueError, 'not in the format'):
1007+ iscsi.assert_valid_iscsi_portal('192.168.1.12:ABCD')
1008+ with self.assertRaisesRegexp(ValueError, 'not in the format'):
1009+ iscsi.assert_valid_iscsi_portal('[fe80::a634:d9ff:fe40:768a]:ABCD')
1010+ with self.assertRaisesRegexp(ValueError, 'not in the format'):
1011+ iscsi.assert_valid_iscsi_portal('test.example.com:ABCD')
1012+
1013+ def test_iscsi_portal_parsing_good_portals(self):
1014+ host, port = iscsi.assert_valid_iscsi_portal('192.168.1.12:9000')
1015+ self.assertEquals(host, '192.168.1.12')
1016+ self.assertEquals(port, 9000)
1017+
1018+ host, port = iscsi.assert_valid_iscsi_portal(
1019+ '[fe80::a634:d9ff:fe40:768a]:9999')
1020+ self.assertEquals(host, 'fe80::a634:d9ff:fe40:768a')
1021+ self.assertEquals(port, 9999)
1022+
1023+ host, port = iscsi.assert_valid_iscsi_portal('test.example.com:8000')
1024+ self.assertEquals(host, 'test.example.com')
1025+ self.assertEquals(port, 8000)
1026+
1027+ # disk specification:
1028+ # TARGETSPEC=host:proto:port:lun:targetname
1029+ # root=iscsi:$TARGETSPEC
1030+ # root=iscsi:user:password@$TARGETSPEC
1031+ # root=iscsi:user:password:initiatoruser:initiatorpassword@$TARGETSPEC
1032+ def test_iscsi_disk_basic(self):
1033+ with self.assertRaisesRegexp(ValueError, 'must be specified'):
1034+ iscsi.IscsiDisk('')
1035+
1036+ # typo
1037+ with self.assertRaisesRegexp(ValueError, 'must be specified'):
1038+ iscsi.IscsiDisk('iscs:')
1039+
1040+ # no specification
1041+ with self.assertRaisesRegexp(ValueError, 'must be specified'):
1042+ iscsi.IscsiDisk('iscsi:')
1043+ with self.assertRaisesRegexp(ValueError, 'Both host and targetname'):
1044+ iscsi.IscsiDisk('iscsi:::::')
1045+
1046+ def test_iscsi_disk_ip_valid(self):
1047+ # these are all misparses we cannot catch trivially
1048+ i = iscsi.IscsiDisk('iscsi:192.168::::target')
1049+ self.assertEquals(i.user, None)
1050+ self.assertEquals(i.password, None)
1051+ self.assertEquals(i.iuser, None)
1052+ self.assertEquals(i.ipassword, None)
1053+ self.assertEquals(i.host, '192.168')
1054+ self.assertEquals(i.proto, '6')
1055+ self.assertEquals(i.port, 3260)
1056+ self.assertEquals(i.lun, 0)
1057+ self.assertEquals(i.target, 'target')
1058+
1059+ i = iscsi.IscsiDisk('iscsi:fe80:::::target')
1060+ self.assertEquals(i.user, None)
1061+ self.assertEquals(i.password, None)
1062+ self.assertEquals(i.iuser, None)
1063+ self.assertEquals(i.ipassword, None)
1064+ self.assertEquals(i.host, 'fe80:')
1065+ self.assertEquals(i.proto, '6')
1066+ self.assertEquals(i.port, 3260)
1067+ self.assertEquals(i.lun, 0)
1068+ self.assertEquals(i.target, 'target')
1069+
1070+ i = iscsi.IscsiDisk('iscsi:test.example::::target')
1071+ self.assertEquals(i.user, None)
1072+ self.assertEquals(i.password, None)
1073+ self.assertEquals(i.iuser, None)
1074+ self.assertEquals(i.ipassword, None)
1075+ self.assertEquals(i.host, 'test.example')
1076+ self.assertEquals(i.proto, '6')
1077+ self.assertEquals(i.port, 3260)
1078+ self.assertEquals(i.lun, 0)
1079+ self.assertEquals(i.target, 'target')
1080+
1081+ def test_iscsi_disk_port(self):
1082+ with self.assertRaisesRegexp(ValueError, 'Specified iSCSI port'):
1083+ iscsi.IscsiDisk('iscsi:192.168.1.12::ABCD::target')
1084+ with self.assertRaisesRegexp(ValueError, 'Specified iSCSI port'):
1085+ iscsi.IscsiDisk('iscsi:fe80::a634:d9ff:fe40:768a:6::ABCD::target')
1086+ with self.assertRaisesRegexp(ValueError, 'Specified iSCSI port'):
1087+ iscsi.IscsiDisk('iscsi:test.example.com::ABCD::target')
1088+
1089+ def test_iscsi_disk_target(self):
1090+ with self.assertRaisesRegexp(ValueError, 'Both host and targetname'):
1091+ iscsi.IscsiDisk('iscsi:192.168.1.12::::')
1092+ with self.assertRaisesRegexp(ValueError, 'Both host and targetname'):
1093+ iscsi.IscsiDisk('iscsi:fe80::a634:d9ff:fe40:768a:6::::')
1094+ with self.assertRaisesRegexp(ValueError, 'Both host and targetname'):
1095+ iscsi.IscsiDisk('iscsi:test.example.com::::')
1096+
1097+ def test_iscsi_disk_ip(self):
1098+ with self.assertRaisesRegexp(ValueError, 'Both host and targetname'):
1099+ iscsi.IscsiDisk('iscsi:::::target')
1100+
1101+ def test_iscsi_disk_auth(self):
1102+ # user without password
1103+ with self.assertRaises(ValueError):
1104+ iscsi.IscsiDisk('iscsi:user@192.168.1.12::::target')
1105+ with self.assertRaises(ValueError):
1106+ iscsi.IscsiDisk('iscsi:user@fe80::a634:d9ff:fe40:768a:6::::target')
1107+ with self.assertRaises(ValueError):
1108+ iscsi.IscsiDisk('iscsi:user@test.example.com::::target')
1109+
1110+ # iuser without password
1111+ with self.assertRaises(ValueError):
1112+ iscsi.IscsiDisk('iscsi:user:password:iuser@192.168.1.12::::target')
1113+ with self.assertRaises(ValueError):
1114+ iscsi.IscsiDisk('iscsi:user:password:iuser@'
1115+ 'fe80::a634:d9ff:fe40:768a:6::::target')
1116+ with self.assertRaises(ValueError):
1117+ iscsi.IscsiDisk(
1118+ 'iscsi:user:password:iuser@test.example.com::::target')
1119+
1120+ def test_iscsi_disk_good_ipv4(self):
1121+ i = iscsi.IscsiDisk('iscsi:192.168.1.12:6:3260:1:target')
1122+ self.assertEquals(i.user, None)
1123+ self.assertEquals(i.password, None)
1124+ self.assertEquals(i.iuser, None)
1125+ self.assertEquals(i.ipassword, None)
1126+ self.assertEquals(i.host, '192.168.1.12')
1127+ self.assertEquals(i.proto, '6')
1128+ self.assertEquals(i.port, 3260)
1129+ self.assertEquals(i.lun, 1)
1130+ self.assertEquals(i.target, 'target')
1131+
1132+ i = iscsi.IscsiDisk('iscsi:192.168.1.12::3260:1:target')
1133+ self.assertEquals(i.user, None)
1134+ self.assertEquals(i.password, None)
1135+ self.assertEquals(i.iuser, None)
1136+ self.assertEquals(i.ipassword, None)
1137+ self.assertEquals(i.host, '192.168.1.12')
1138+ self.assertEquals(i.proto, '6')
1139+ self.assertEquals(i.port, 3260)
1140+ self.assertEquals(i.lun, 1)
1141+ self.assertEquals(i.target, 'target')
1142+
1143+ i = iscsi.IscsiDisk('iscsi:192.168.1.12:::1:target')
1144+ self.assertEquals(i.user, None)
1145+ self.assertEquals(i.password, None)
1146+ self.assertEquals(i.iuser, None)
1147+ self.assertEquals(i.ipassword, None)
1148+ self.assertEquals(i.host, '192.168.1.12')
1149+ self.assertEquals(i.proto, '6')
1150+ self.assertEquals(i.port, 3260)
1151+ self.assertEquals(i.lun, 1)
1152+ self.assertEquals(i.target, 'target')
1153+
1154+ i = iscsi.IscsiDisk('iscsi:user:password@192.168.1.12:::1:target')
1155+ self.assertEquals(i.user, 'user')
1156+ self.assertEquals(i.password, 'password')
1157+ self.assertEquals(i.iuser, None)
1158+ self.assertEquals(i.ipassword, None)
1159+ self.assertEquals(i.host, '192.168.1.12')
1160+ self.assertEquals(i.proto, '6')
1161+ self.assertEquals(i.port, 3260)
1162+ self.assertEquals(i.lun, 1)
1163+ self.assertEquals(i.target, 'target')
1164+
1165+ i = iscsi.IscsiDisk('iscsi:user:@192.168.1.12:::1:target')
1166+ self.assertEquals(i.user, 'user')
1167+ self.assertEquals(i.password, '')
1168+ self.assertEquals(i.iuser, None)
1169+ self.assertEquals(i.ipassword, None)
1170+ self.assertEquals(i.host, '192.168.1.12')
1171+ self.assertEquals(i.proto, '6')
1172+ self.assertEquals(i.port, 3260)
1173+ self.assertEquals(i.lun, 1)
1174+ self.assertEquals(i.target, 'target')
1175+
1176+ i = iscsi.IscsiDisk('iscsi:user:password:iuser:ipassword@'
1177+ '192.168.1.12:::1:target')
1178+ self.assertEquals(i.user, 'user')
1179+ self.assertEquals(i.password, 'password')
1180+ self.assertEquals(i.iuser, 'iuser')
1181+ self.assertEquals(i.ipassword, 'ipassword')
1182+ self.assertEquals(i.host, '192.168.1.12')
1183+ self.assertEquals(i.proto, '6')
1184+ self.assertEquals(i.port, 3260)
1185+ self.assertEquals(i.lun, 1)
1186+ self.assertEquals(i.target, 'target')
1187+
1188+ i = iscsi.IscsiDisk('iscsi:user:password:iuser:@'
1189+ '192.168.1.12:::1:target')
1190+ self.assertEquals(i.user, 'user')
1191+ self.assertEquals(i.password, 'password')
1192+ self.assertEquals(i.iuser, 'iuser')
1193+ self.assertEquals(i.ipassword, '')
1194+ self.assertEquals(i.host, '192.168.1.12')
1195+ self.assertEquals(i.proto, '6')
1196+ self.assertEquals(i.port, 3260)
1197+ self.assertEquals(i.lun, 1)
1198+ self.assertEquals(i.target, 'target')
1199+
1200+ i = iscsi.IscsiDisk('iscsi:user::iuser:@192.168.1.12:::1:target')
1201+ self.assertEquals(i.user, 'user')
1202+ self.assertEquals(i.password, '')
1203+ self.assertEquals(i.iuser, 'iuser')
1204+ self.assertEquals(i.ipassword, '')
1205+ self.assertEquals(i.host, '192.168.1.12')
1206+ self.assertEquals(i.proto, '6')
1207+ self.assertEquals(i.port, 3260)
1208+ self.assertEquals(i.lun, 1)
1209+ self.assertEquals(i.target, 'target')
1210+
1211+ def test_iscsi_disk_good_ipv6(self):
1212+ i = iscsi.IscsiDisk(
1213+ 'iscsi:[fe80::a634:d9ff:fe40:768a:6]:5:3260:1:target')
1214+ self.assertEquals(i.user, None)
1215+ self.assertEquals(i.password, None)
1216+ self.assertEquals(i.iuser, None)
1217+ self.assertEquals(i.ipassword, None)
1218+ self.assertEquals(i.host, 'fe80::a634:d9ff:fe40:768a:6')
1219+ self.assertEquals(i.proto, '6')
1220+ self.assertEquals(i.port, 3260)
1221+ self.assertEquals(i.lun, 1)
1222+ self.assertEquals(i.target, 'target')
1223+
1224+ i = iscsi.IscsiDisk(
1225+ 'iscsi:[fe80::a634:d9ff:fe40:768a:6]::3260:1:target')
1226+ self.assertEquals(i.user, None)
1227+ self.assertEquals(i.password, None)
1228+ self.assertEquals(i.iuser, None)
1229+ self.assertEquals(i.ipassword, None)
1230+ self.assertEquals(i.host, 'fe80::a634:d9ff:fe40:768a:6')
1231+ self.assertEquals(i.proto, '6')
1232+ self.assertEquals(i.port, 3260)
1233+ self.assertEquals(i.lun, 1)
1234+ self.assertEquals(i.target, 'target')
1235+
1236+ i = iscsi.IscsiDisk('iscsi:[fe80::a634:d9ff:fe40:768a:6]:::1:target')
1237+ self.assertEquals(i.user, None)
1238+ self.assertEquals(i.password, None)
1239+ self.assertEquals(i.iuser, None)
1240+ self.assertEquals(i.ipassword, None)
1241+ self.assertEquals(i.host, 'fe80::a634:d9ff:fe40:768a:6')
1242+ self.assertEquals(i.proto, '6')
1243+ self.assertEquals(i.port, 3260)
1244+ self.assertEquals(i.lun, 1)
1245+ self.assertEquals(i.target, 'target')
1246+
1247+ i = iscsi.IscsiDisk('iscsi:user:password@'
1248+ '[fe80::a634:d9ff:fe40:768a:6]:::1:target')
1249+ self.assertEquals(i.user, 'user')
1250+ self.assertEquals(i.password, 'password')
1251+ self.assertEquals(i.iuser, None)
1252+ self.assertEquals(i.ipassword, None)
1253+ self.assertEquals(i.host, 'fe80::a634:d9ff:fe40:768a:6')
1254+ self.assertEquals(i.proto, '6')
1255+ self.assertEquals(i.port, 3260)
1256+ self.assertEquals(i.lun, 1)
1257+ self.assertEquals(i.target, 'target')
1258+
1259+ i = iscsi.IscsiDisk('iscsi:user:@'
1260+ '[fe80::a634:d9ff:fe40:768a:6]:::1:target')
1261+ self.assertEquals(i.user, 'user')
1262+ self.assertEquals(i.password, '')
1263+ self.assertEquals(i.iuser, None)
1264+ self.assertEquals(i.ipassword, None)
1265+ self.assertEquals(i.host, 'fe80::a634:d9ff:fe40:768a:6')
1266+ self.assertEquals(i.proto, '6')
1267+ self.assertEquals(i.port, 3260)
1268+ self.assertEquals(i.lun, 1)
1269+ self.assertEquals(i.target, 'target')
1270+
1271+ i = iscsi.IscsiDisk('iscsi:user:password:iuser:ipassword@'
1272+ '[fe80::a634:d9ff:fe40:768a:6]:::1:target')
1273+ self.assertEquals(i.user, 'user')
1274+ self.assertEquals(i.password, 'password')
1275+ self.assertEquals(i.iuser, 'iuser')
1276+ self.assertEquals(i.ipassword, 'ipassword')
1277+ self.assertEquals(i.host, 'fe80::a634:d9ff:fe40:768a:6')
1278+ self.assertEquals(i.proto, '6')
1279+ self.assertEquals(i.port, 3260)
1280+ self.assertEquals(i.lun, 1)
1281+ self.assertEquals(i.target, 'target')
1282+
1283+ i = iscsi.IscsiDisk('iscsi:user:password:iuser:@'
1284+ '[fe80::a634:d9ff:fe40:768a:6]:::1:target')
1285+ self.assertEquals(i.user, 'user')
1286+ self.assertEquals(i.password, 'password')
1287+ self.assertEquals(i.iuser, 'iuser')
1288+ self.assertEquals(i.ipassword, '')
1289+ self.assertEquals(i.host, 'fe80::a634:d9ff:fe40:768a:6')
1290+ self.assertEquals(i.proto, '6')
1291+ self.assertEquals(i.port, 3260)
1292+ self.assertEquals(i.lun, 1)
1293+ self.assertEquals(i.target, 'target')
1294+
1295+ i = iscsi.IscsiDisk('iscsi:user::iuser:@'
1296+ '[fe80::a634:d9ff:fe40:768a:6]:::1:target')
1297+ self.assertEquals(i.user, 'user')
1298+ self.assertEquals(i.password, '')
1299+ self.assertEquals(i.iuser, 'iuser')
1300+ self.assertEquals(i.ipassword, '')
1301+ self.assertEquals(i.host, 'fe80::a634:d9ff:fe40:768a:6')
1302+ self.assertEquals(i.proto, '6')
1303+ self.assertEquals(i.port, 3260)
1304+ self.assertEquals(i.lun, 1)
1305+ self.assertEquals(i.target, 'target')
1306+
1307+ def test_iscsi_disk_good_hostname(self):
1308+ i = iscsi.IscsiDisk('iscsi:test.example.com:6:3260:1:target')
1309+ self.assertEquals(i.user, None)
1310+ self.assertEquals(i.password, None)
1311+ self.assertEquals(i.iuser, None)
1312+ self.assertEquals(i.ipassword, None)
1313+ self.assertEquals(i.host, 'test.example.com')
1314+ self.assertEquals(i.proto, '6')
1315+ self.assertEquals(i.port, 3260)
1316+ self.assertEquals(i.lun, 1)
1317+ self.assertEquals(i.target, 'target')
1318+
1319+ i = iscsi.IscsiDisk('iscsi:test.example.com::3260:1:target')
1320+ self.assertEquals(i.user, None)
1321+ self.assertEquals(i.password, None)
1322+ self.assertEquals(i.iuser, None)
1323+ self.assertEquals(i.ipassword, None)
1324+ self.assertEquals(i.host, 'test.example.com')
1325+ self.assertEquals(i.proto, '6')
1326+ self.assertEquals(i.port, 3260)
1327+ self.assertEquals(i.lun, 1)
1328+ self.assertEquals(i.target, 'target')
1329+
1330+ i = iscsi.IscsiDisk('iscsi:test.example.com:::1:target')
1331+ self.assertEquals(i.user, None)
1332+ self.assertEquals(i.password, None)
1333+ self.assertEquals(i.iuser, None)
1334+ self.assertEquals(i.ipassword, None)
1335+ self.assertEquals(i.host, 'test.example.com')
1336+ self.assertEquals(i.proto, '6')
1337+ self.assertEquals(i.port, 3260)
1338+ self.assertEquals(i.lun, 1)
1339+ self.assertEquals(i.target, 'target')
1340+
1341+ i = iscsi.IscsiDisk('iscsi:user:password@test.example.com:::1:target')
1342+ self.assertEquals(i.user, 'user')
1343+ self.assertEquals(i.password, 'password')
1344+ self.assertEquals(i.iuser, None)
1345+ self.assertEquals(i.ipassword, None)
1346+ self.assertEquals(i.host, 'test.example.com')
1347+ self.assertEquals(i.proto, '6')
1348+ self.assertEquals(i.port, 3260)
1349+ self.assertEquals(i.lun, 1)
1350+ self.assertEquals(i.target, 'target')
1351+
1352+ i = iscsi.IscsiDisk('iscsi:user:@test.example.com:::1:target')
1353+ self.assertEquals(i.user, 'user')
1354+ self.assertEquals(i.password, '')
1355+ self.assertEquals(i.iuser, None)
1356+ self.assertEquals(i.ipassword, None)
1357+ self.assertEquals(i.host, 'test.example.com')
1358+ self.assertEquals(i.proto, '6')
1359+ self.assertEquals(i.port, 3260)
1360+ self.assertEquals(i.lun, 1)
1361+ self.assertEquals(i.target, 'target')
1362+
1363+ i = iscsi.IscsiDisk('iscsi:user:password:iuser:ipassword@'
1364+ 'test.example.com:::1:target')
1365+ self.assertEquals(i.user, 'user')
1366+ self.assertEquals(i.password, 'password')
1367+ self.assertEquals(i.iuser, 'iuser')
1368+ self.assertEquals(i.ipassword, 'ipassword')
1369+ self.assertEquals(i.host, 'test.example.com')
1370+ self.assertEquals(i.proto, '6')
1371+ self.assertEquals(i.port, 3260)
1372+ self.assertEquals(i.lun, 1)
1373+ self.assertEquals(i.target, 'target')
1374+
1375+ i = iscsi.IscsiDisk('iscsi:user:password:iuser:@'
1376+ 'test.example.com:::1:target')
1377+ self.assertEquals(i.user, 'user')
1378+ self.assertEquals(i.password, 'password')
1379+ self.assertEquals(i.iuser, 'iuser')
1380+ self.assertEquals(i.ipassword, '')
1381+ self.assertEquals(i.host, 'test.example.com')
1382+ self.assertEquals(i.proto, '6')
1383+ self.assertEquals(i.port, 3260)
1384+ self.assertEquals(i.lun, 1)
1385+ self.assertEquals(i.target, 'target')
1386+
1387+ i = iscsi.IscsiDisk('iscsi:user::iuser:@test.example.com:::1:target')
1388+ self.assertEquals(i.user, 'user')
1389+ self.assertEquals(i.password, '')
1390+ self.assertEquals(i.iuser, 'iuser')
1391+ self.assertEquals(i.ipassword, '')
1392+ self.assertEquals(i.host, 'test.example.com')
1393+ self.assertEquals(i.proto, '6')
1394+ self.assertEquals(i.port, 3260)
1395+ self.assertEquals(i.lun, 1)
1396+ self.assertEquals(i.target, 'target')
1397+
1398+# vi: ts=4 expandtab syntax=python
1399
1400=== modified file 'tests/unittests/test_util.py'
1401--- tests/unittests/test_util.py 2017-02-10 20:55:59 +0000
1402+++ tests/unittests/test_util.py 2017-02-22 16:43:05 +0000
1403@@ -543,4 +543,29 @@
1404 self.assertEqual(type(loaded_contents), bytes)
1405 self.assertEqual(loaded_contents, contents)
1406
1407+
1408+class TestIpAddress(TestCase):
1409+ """Test utility 'is_valid_ip{,v4,v6}_address'"""
1410+
1411+ def test_is_valid_ipv6_address(self):
1412+ self.assertFalse(util.is_valid_ipv6_address('192.168'))
1413+ self.assertFalse(util.is_valid_ipv6_address('69.89.31.226'))
1414+ self.assertFalse(util.is_valid_ipv6_address('254.254.254.254'))
1415+ self.assertTrue(util.is_valid_ipv6_address('2001:db8::1'))
1416+ self.assertTrue(util.is_valid_ipv6_address('::1'))
1417+ self.assertTrue(util.is_valid_ipv6_address(
1418+ '1200:0000:AB00:1234:0000:2552:7777:1313'))
1419+ self.assertFalse(util.is_valid_ipv6_address(
1420+ '1200::AB00:1234::2552:7777:1313'))
1421+ self.assertTrue(util.is_valid_ipv6_address(
1422+ '21DA:D3:0:2F3B:2AA:FF:FE28:9C5A'))
1423+ self.assertFalse(util.is_valid_ipv6_address(
1424+ '1200:0000:AB00:1234:O000:2552:7777:1313'))
1425+ self.assertTrue(util.is_valid_ipv6_address(
1426+ '2002:4559:1FE2::4559:1FE2'))
1427+ self.assertTrue(util.is_valid_ipv6_address(
1428+ '2002:4559:1fe2:0:0:0:4559:1fe2'))
1429+ self.assertTrue(util.is_valid_ipv6_address(
1430+ '2002:4559:1FE2:0000:0000:0000:4559:1FE2'))
1431+
1432 # vi: ts=4 expandtab syntax=python
1433
1434=== modified file 'tests/vmtests/__init__.py'
1435--- tests/vmtests/__init__.py 2017-02-10 16:16:25 +0000
1436+++ tests/vmtests/__init__.py 2017-02-22 16:43:05 +0000
1437@@ -8,11 +8,13 @@
1438 import re
1439 import shutil
1440 import subprocess
1441+import tempfile
1442 import textwrap
1443 import time
1444 import yaml
1445 import curtin.net as curtin_net
1446 import curtin.util as util
1447+from curtin.block import iscsi
1448
1449 from .report_webhook_logger import CaptureReporting
1450 from curtin.commands.install import INSTALL_PASS_MSG
1451@@ -340,6 +342,7 @@
1452 multipath = False
1453 multipath_num_paths = 2
1454 nvme_disks = []
1455+ iscsi_disks = []
1456 recorded_errors = 0
1457 recorded_failures = 0
1458 uefi = False
1459@@ -381,6 +384,102 @@
1460 return ftypes
1461
1462 @classmethod
1463+ def build_iscsi_disks(cls):
1464+ cls._iscsi_disks = list()
1465+ disks = []
1466+ if len(cls.iscsi_disks) == 0:
1467+ return disks
1468+
1469+ portal = os.environ.get("CURTIN_VMTEST_ISCSI_PORTAL", False)
1470+ if not portal:
1471+ raise SkipTest("No iSCSI portal specified in the "
1472+ "environment (CURTIN_VMTEST_ISCSI_PORTAL). "
1473+ "Skipping iSCSI tests.")
1474+
1475+ # note that TGT_IPC_SOCKET also needs to be set for
1476+ # successful communication
1477+
1478+ try:
1479+ cls.tgtd_ip, cls.tgtd_port = \
1480+ iscsi.assert_valid_iscsi_portal(portal)
1481+ except ValueError as e:
1482+ raise ValueError("CURTIN_VMTEST_ISCSI_PORTAL is invalid: %s", e)
1483+
1484+ # copy testcase YAML to a temporary file in order to replace
1485+ # placeholders
1486+ temp_yaml = tempfile.NamedTemporaryFile(prefix=cls.td.tmpdir + '/',
1487+ mode='w+t', delete=False)
1488+ logger.debug("iSCSI YAML is at %s" % temp_yaml.name)
1489+ shutil.copyfile(cls.conf_file, temp_yaml.name)
1490+ cls.conf_file = temp_yaml.name
1491+
1492+ # we implicitly assume testcase YAML is in the same order as
1493+ # iscsi_disks in the testcase
1494+ # path:size:block_size:serial=,port=,cport=
1495+ for (disk_no, disk_dict) in enumerate(cls.iscsi_disks):
1496+ try:
1497+ disk_sz = disk_dict['size']
1498+ except KeyError:
1499+ raise ValueError('No size specified for iSCSI disk')
1500+ try:
1501+ disk_user, disk_password = disk_dict['auth'].split(':')
1502+ if len(disk_user) == 0 and len(disk_password) > 0:
1503+ raise ValueError('Specifying iSCSI target password '
1504+ 'without user is invalid')
1505+ except KeyError:
1506+ disk_user = ''
1507+ disk_password = ''
1508+
1509+ try:
1510+ disk_iuser, disk_ipassword = disk_dict['iauth'].split(':')
1511+ if len(disk_iuser) == 0 and len(disk_ipassword) > 0:
1512+ raise ValueError('Specifying iSCSI initiator password '
1513+ 'without user is invalid')
1514+ except KeyError:
1515+ disk_iuser = ''
1516+ disk_ipassword = ''
1517+
1518+ uuid, _ = util.subp(['uuidgen'], capture=True,
1519+ decode='replace')
1520+ uuid = uuid.rstrip()
1521+ target = 'curtin-%s' % uuid
1522+ cls._iscsi_disks.append(target)
1523+ dpath = os.path.join(cls.td.disks, '%s.img' % (target))
1524+ iscsi_disk = '{}:{}:iscsi:{}:{}:{}:{}:{}:{}'.format(
1525+ dpath, disk_sz, cls.disk_block_size, target,
1526+ disk_user, disk_password, disk_iuser, disk_ipassword)
1527+ disks.extend(['--disk', iscsi_disk])
1528+
1529+ # replace next __RFC4173__ placeholder in YAML
1530+ with tempfile.NamedTemporaryFile(mode='w+t') as temp_yaml:
1531+ shutil.copyfile(cls.conf_file, temp_yaml.name)
1532+ with open(cls.conf_file, 'w+t') as conf:
1533+ replaced = False
1534+ for line in temp_yaml:
1535+ if not replaced and '__RFC4173__' in line:
1536+ actual_rfc4173 = ''
1537+ if len(disk_user) > 0:
1538+ actual_rfc4173 += '%s:%s' % (disk_user,
1539+ disk_password)
1540+ if len(disk_iuser) > 0:
1541+ # empty target user/password
1542+ if len(actual_rfc4173) == 0:
1543+ actual_rfc4173 += ':'
1544+ actual_rfc4173 += ':%s:%s' % (disk_iuser,
1545+ disk_ipassword)
1546+ # any auth specified?
1547+ if len(actual_rfc4173) > 0:
1548+ actual_rfc4173 += '@'
1549+ # assumes LUN 1
1550+ actual_rfc4173 += '%s::%s:1:%s' % (
1551+ cls.tgtd_ip,
1552+ cls.tgtd_port, target)
1553+ line = line.replace('__RFC4173__', actual_rfc4173)
1554+ replaced = True
1555+ conf.write(line)
1556+ return disks
1557+
1558+ @classmethod
1559 def setUpClass(cls):
1560 # check if we should skip due to host arch
1561 if cls.arch in cls.arch_skip:
1562@@ -492,6 +591,9 @@
1563 "serial=nvme-%d" % disk_no)
1564 disks.extend(['--disk', nvme_disk])
1565
1566+ # build iscsi disk args if needed
1567+ disks.extend(cls.build_iscsi_disks())
1568+
1569 # proxy config
1570 configs = [cls.conf_file]
1571 cls.proxy = get_apt_proxy()
1572@@ -635,6 +737,9 @@
1573 dpath, disk_driver, TARGET_IMAGE_FORMAT, bsize_args)
1574 nvme_disks.extend([disk])
1575
1576+ # unlike NVMe disks, we do not want to configure the iSCSI disks
1577+ # via KVM, which would use qemu's iSCSI target layer.
1578+
1579 if cls.multipath:
1580 target_disks = target_disks * cls.multipath_num_paths
1581 extra_disks = extra_disks * cls.multipath_num_paths
1582@@ -697,6 +802,41 @@
1583 cls.__name__, time.time() - setup_start)
1584
1585 @classmethod
1586+ def cleanIscsiState(cls, result, keep_pass, keep_fail):
1587+ if result:
1588+ keep = keep_pass
1589+ else:
1590+ keep = keep_fail
1591+
1592+ if 'disks' in keep or (len(keep) == 1 and keep[0] == 'all'):
1593+ logger.info('Not removing iSCSI disks from tgt, they will '
1594+ 'need to be removed manually.')
1595+ else:
1596+ for target in cls._iscsi_disks:
1597+ logger.debug('Removing iSCSI target %s', target)
1598+ tgtadm_out, _ = util.subp(
1599+ ['tgtadm', '--lld=iscsi', '--mode=target', '--op=show'],
1600+ capture=True)
1601+
1602+ # match target name to TID, e.g.:
1603+ # Target 4: curtin-59b5507d-1a6d-4b15-beda-3484f2a7d399
1604+ tid = None
1605+ for line in tgtadm_out.splitlines():
1606+ # new target stanza
1607+ m = re.match(r'Target (\d+): (\S+)', line)
1608+ if m and target in m.group(2):
1609+ tid = m.group(1)
1610+ break
1611+
1612+ if tid:
1613+ util.subp(['tgtadm', '--lld=iscsi', '--mode=target',
1614+ '--tid=%s' % tid, '--op=delete'])
1615+ else:
1616+ logger.warn('Unable to determine target ID for '
1617+ 'target %s. It will need to be manually '
1618+ 'removed.', target)
1619+
1620+ @classmethod
1621 def tearDownClass(cls):
1622 success = False
1623 sfile = os.path.exists(cls.td.success_file)
1624@@ -713,6 +853,10 @@
1625 keep_pass=KEEP_DATA['pass'],
1626 keep_fail=KEEP_DATA['fail'])
1627
1628+ cls.cleanIscsiState(success,
1629+ keep_pass=KEEP_DATA['pass'],
1630+ keep_fail=KEEP_DATA['fail'])
1631+
1632 @classmethod
1633 def expected_interfaces(cls):
1634 expected = []
1635
1636=== added file 'tests/vmtests/test_iscsi.py'
1637--- tests/vmtests/test_iscsi.py 1970-01-01 00:00:00 +0000
1638+++ tests/vmtests/test_iscsi.py 2017-02-22 16:43:05 +0000
1639@@ -0,0 +1,51 @@
1640+from . import VMBaseClass
1641+from .releases import base_vm_classes as relbase
1642+
1643+import textwrap
1644+
1645+
1646+class TestBasicIscsiAbs(VMBaseClass):
1647+ interactive = False
1648+ iscsi_disks = [
1649+ {'size': '3G'},
1650+ {'size': '4G', 'auth': 'user:passw0rd'},
1651+ {'size': '5G', 'auth': 'user:passw0rd', 'iauth': 'iuser:ipassw0rd'},
1652+ {'size': '6G', 'iauth': 'iuser:ipassw0rd'}]
1653+ conf_file = "examples/tests/basic_iscsi.yaml"
1654+
1655+ collect_scripts = [textwrap.dedent(
1656+ """
1657+ cd OUTPUT_COLLECT_D
1658+ cat /etc/fstab > fstab
1659+ ls /dev/disk/by-dname/ > ls_dname
1660+ find /etc/network/interfaces.d > find_interfacesd
1661+ cat /mnt/iscsi1/testfile > testfile1
1662+ cat /mnt/iscsi2/testfile > testfile2
1663+ cat /mnt/iscsi3/testfile > testfile3
1664+ cat /mnt/iscsi4/testfile > testfile4
1665+ """)]
1666+
1667+ def test_output_files_exist(self):
1668+ # add check by SN or UUID that the iSCSI disks are attached?
1669+ self.output_files_exist(["fstab", "testfile1", "testfile2",
1670+ "testfile3", "testfile4"])
1671+
1672+
1673+class PreciseTestIscsiBasic(relbase.precise, TestBasicIscsiAbs):
1674+ __test__ = True
1675+
1676+
1677+class TrustyTestIscsiBasic(relbase.trusty, TestBasicIscsiAbs):
1678+ __test__ = True
1679+
1680+
1681+class XenialTestIscsiBasic(relbase.xenial, TestBasicIscsiAbs):
1682+ __test__ = True
1683+
1684+
1685+class YakketyTestIscsiBasic(relbase.yakkety, TestBasicIscsiAbs):
1686+ __test__ = True
1687+
1688+
1689+class ZestyTestIscsiBasic(relbase.zesty, TestBasicIscsiAbs):
1690+ __test__ = True
1691
1692=== added file 'tools/find-tgt'
1693--- tools/find-tgt 1970-01-01 00:00:00 +0000
1694+++ tools/find-tgt 2017-02-22 16:43:05 +0000
1695@@ -0,0 +1,124 @@
1696+#!/bin/bash
1697+TGT_PID=""
1698+fail() { [ $# -eq 0 ] || error "$@"; exit 1; }
1699+error() { echo "$@" 1>&2; }
1700+cleanup() {
1701+ [ -z "$TGT_PID" ] || kill -9 "$TGT_PID"
1702+}
1703+
1704+Usage() {
1705+ cat <<EOF
1706+Usage: ${0##*/} out_dir [ipv4addr]
1707+
1708+ Start a tgt server on a random port, listening on address ipv4addr.
1709+ If ipv4addr not provided, use the address of the interface
1710+ with the default route.
1711+
1712+ if out_d does not exist it will be created.
1713+
1714+ Writes the following files in out_d:
1715+ pid: pidfile for the started tgt
1716+ tgt.log: log file
1717+ socket: the socket used for TGT_IPC_SOCKET
1718+ info: bash snippet to export TGT_IPC_SOCKET and other info.
1719+EOF
1720+}
1721+
1722+find_ipv4addr() {
1723+ # tgtd/tgtadmin end up using a suffix from here of the control port
1724+ local dev="" addr=""
1725+ dev=$(route -n | awk '$1 == "0.0.0.0" { print $8 }')
1726+ [ -n "$dev" ] || { error "failed to find ipv4 device"; return 1; }
1727+ addr=$(ip addr show dev "$dev" |
1728+ awk '$1 == "inet" {gsub(/\/.*/,"", $2); print $2; exit}')
1729+ case "$addr" in
1730+ *.*.*.*) :;;
1731+ *) error "failed to get ipv4addr on dev '$dev'. got '$addr'"
1732+ return 1;;
1733+ esac
1734+ _RET=$addr
1735+}
1736+
1737+if [ "$1" = "--help" -o "$1" = "-h" ]; then
1738+ Usage;
1739+ exit 0;
1740+elif [ $# -eq 0 ]; then
1741+ Usage 1>&2;
1742+ fail "Must provide a directory"
1743+fi
1744+
1745+trap cleanup EXIT
1746+out_d="$1"
1747+ipv4addr=${2}
1748+
1749+[ -d "$out_d" ] || mkdir -p "$out_d" ||
1750+ fail "'$out_d' not a directory, and could not create"
1751+out_d=$(cd "$out_d" && pwd)
1752+log="${out_d}/tgt.log"
1753+info="${out_d}/info"
1754+socket="${out_d}/socket"
1755+pidfile="${out_d}/pid"
1756+
1757+command -v tgtd >/dev/null 2>&1 || fail "no tgtd command"
1758+
1759+if [ -z "$ipv4addr" ]; then
1760+ find_ipv4addr || fail
1761+ ipv4addr="$_RET"
1762+fi
1763+
1764+if [ -e "$pidfile" ] && read oldpid < "$pidfile" &&
1765+ [ -e "/proc/$oldpid" ]; then
1766+ echo "killing old pid $oldpid"
1767+ kill -9 $oldpid
1768+fi
1769+rm -f "$socket" "$pidfile" "$info" "$log"
1770+: > "$log" || fail "failed to write to $log"
1771+
1772+# Racily try for a port. Annoyingly, tgtd doesn't fail when it's
1773+# unable to use the requested portal, it just happily uses the default
1774+tries=1
1775+while [ $tries -le 100 ]; do
1776+ # pick a port > 1024, and I think tgt needs one < 2^15 (32768)
1777+ port=$((RANDOM+1024))
1778+ [ "$port" -lt 32768 ] || continue
1779+
1780+ portal="$ipv4addr:$port"
1781+ error "going for $portal"
1782+ TGT_IPC_SOCKET="$socket" \
1783+ tgtd --foreground --iscsi "portal=$portal" >"$log" 2>&1 &
1784+ TGT_PID=$!
1785+ pid=$TGT_PID
1786+
1787+ # did we succesfully attach to the correct port?
1788+ out=$(netstat --program --all --numeric 2>/dev/null) ||
1789+ fail "failed to netstat --program --all --numeric"
1790+ pidstgt=$(echo "$out" |
1791+ awk '$4 == portal { print $7; exit(0); }' "portal=$portal")
1792+
1793+ if [ -n "$pidstgt" ]; then
1794+ if [ "$pidstgt" != "$pid/tgtd" ]; then
1795+ error "'$pidstgt' was listening on $portal, not $pid"
1796+ else
1797+ error "grabbed $port in $pid on try $tries"
1798+ {
1799+ echo "export TGT_PID=$pid"
1800+ echo "export TGT_IPC_SOCKET=$socket"
1801+ echo "export CURTIN_VMTEST_ISCSI_PORTAL=$portal"
1802+ } > "$info"
1803+ echo "$pid" > "$pidfile"
1804+ error "To list targets on this tgt: "
1805+ error " TGT_IPC_SOCKET=$socket tgtadm --lld=iscsi --mode=target --op=show"
1806+ error "For a client view of visible disks:"
1807+ error " iscsiadm --mode=discovery --type=sendtargets --portal=$portal"
1808+ error " iscsiadm --mode=node --portal=$portal --op=show"
1809+ TGT_PID=""
1810+ exit 0
1811+ fi
1812+ else
1813+ error "nothing listening on $portal pid=$pid"
1814+ fi
1815+ kill -9 $pid >/dev/null 2>&1
1816+ TGT_PID=""
1817+ wait $pid
1818+ tries=$(($tries+1))
1819+done
1820
1821=== modified file 'tools/jenkins-runner'
1822--- tools/jenkins-runner 2017-01-27 16:13:17 +0000
1823+++ tools/jenkins-runner 2017-02-22 16:43:05 +0000
1824@@ -53,6 +53,13 @@
1825 pargs=( --process-timeout=86400 "--processes=$parallel" )
1826 fi
1827
1828+if [ -z "$TGT_IPC_SOCKET" ] && command -v "tgtd" >/dev/null 2>&1; then
1829+ tgtdir="$topdir/tgt.d"
1830+ ./tools/find-tgt "$tgtdir" ||
1831+ fail "could not start a tgt service"
1832+ . "$tgtdir/info"
1833+fi
1834+
1835 # avoid LOG info by running python3 tests/vmtests/image_sync.py
1836 # rather than python3 -m tests.vmtests.image_sync (LP: #1594465)
1837 echo "Working with images in $IMAGE_DIR"
1838@@ -67,6 +74,8 @@
1839 ret=$?
1840 end_s=$(date +%s)
1841 echo "$(date -R): vmtest end [$ret] in $(($end_s-$start_s))s"
1842+[ $ret -eq 0 ] && [[ $pkeep == *"all"* ]] || [ -n "${TGT_PID}" ] && kill -9 ${TGT_PID}
1843+[ $ret -ne 0 ] && [[ $pfail == *"all"* ]] || [ -n "${TGT_PID}" ] && kill -9 ${TGT_PID}
1844 exit $ret
1845
1846 # vi: ts=4 expandtab syntax=sh
1847
1848=== modified file 'tools/launch'
1849--- tools/launch 2017-01-20 19:15:34 +0000
1850+++ tools/launch 2017-02-22 16:43:05 +0000
1851@@ -249,6 +249,105 @@
1852 return 1
1853 }
1854
1855+get_tgt_tid() {
1856+ # retry until we successfully register a tid in tgtadm, return it.
1857+ local target="$1" src="$2" out="" tid="" lun="1" ret=""
1858+ while true; do
1859+ # use next target ID
1860+ # this is racy, potentially, but we iterate until it works
1861+ out=$(tgtadm --lld=iscsi --mode=target --op=show) ||
1862+ { error "Failed to show iscsi devices"; return 1; }
1863+
1864+ # are we re-using a target?
1865+ tid=$(echo "$out" |
1866+ awk -F' ' "/^Target.*$target/ {gsub(\":\",\"\",\$2); print \$2; exit;} ")
1867+
1868+ [ -n "${tid}" ] && {
1869+ _RET="$tid"
1870+ return 0
1871+ }
1872+
1873+ tid=$(echo "$out" |
1874+ awk -F' ' '/^Target/ {tid=$2} END{gsub(":","",tid); print tid+1}')
1875+
1876+ tgtadm --lld=iscsi --mode=target --op=new "--tid=$tid" \
1877+ "--targetname=$target" || {
1878+ ret=$?
1879+ debug 1 "failed [$ret] in attempt to register tid=$tid " \
1880+ "targetname=$target"
1881+ sleep 0.1
1882+ continue
1883+ }
1884+
1885+ # assume LUN 1?
1886+ tgtadm --lld=iscsi --mode=logicalunit --op=new "--tid=$tid" \
1887+ "--backing-store=$src" --device-type=disk "--lun=$lun" || {
1888+ error "Unable to create TGT LUN $lun backed by '$src'. tid=$tid."
1889+ error "Does a prior config need to be cleaned up?"
1890+ return 1
1891+ }
1892+
1893+ # set all initiators to be able to authenticate
1894+ tgtadm --lld=iscsi --mode=target --op=bind "--tid=$tid" -I ALL || {
1895+ error "Unable to set TGT target ${tid} ACL to ALL."
1896+ error "Does a prior config need to be cleaned up?"
1897+ return 1
1898+ }
1899+
1900+ _RET="$tid"
1901+ return 0
1902+ done
1903+}
1904+
1905+configure_tgt_auth() {
1906+ # update a tgt target with authentication if specified
1907+ local tid="$1" user="$2" password="$3" iuser="$4" ipassword="$5"
1908+
1909+ if [ -n "$user" ]; then
1910+ tgtadm --lld=iscsi --mode=account --op=show | grep -q "${user}"
1911+ if [ $? != 0 ]; then
1912+ tgtadm --lld=iscsi --mode=account --op=new "--user=${user}" \
1913+ "--password=${password}" || {
1914+ RC=$?
1915+ error "Unable to create TGT user (${user}:${password}): ${RC}"
1916+ error "Does a prior config need to be cleaned up?"
1917+ return 1
1918+ }
1919+ fi
1920+ tgtadm --lld=iscsi --mode=account --op=bind "--tid=$tid" \
1921+ "--user=${user}" || {
1922+ RC=$?
1923+ error "Unable to set TGT target ${tid} target auth " \
1924+ "user to ${user}: ${RC}."
1925+ error "Does a prior config need to be cleaned up?"
1926+ return 1
1927+ }
1928+ fi
1929+
1930+ if [ -n "$iuser" ]; then
1931+ tgtadm --lld=iscsi --mode=account --op=show | grep -q "${iuser}"
1932+ if [ $? != 0 ]; then
1933+ tgtadm --lld=iscsi --mode=account --op=new "--user=${iuser}" \
1934+ "--password=${ipassword}" || {
1935+ RC=$?
1936+ error "Unable to create TGT user (${iuser}:${ipassword}): ${RC}"
1937+ error "Does a prior config need to be cleaned up?"
1938+ return 1
1939+ }
1940+ fi
1941+ tgtadm --lld=iscsi --mode=account --op=bind "--tid=$tid" \
1942+ "--user=${iuser}" --outgoing || {
1943+ RC=$?
1944+ error "Unable to set TGT target ${tid} initiator user " \
1945+ "to ${iuser}: ${RC}."
1946+ error "Does a prior config need to be cleaned up?"
1947+ return 1
1948+ }
1949+ fi
1950+
1951+ return 0
1952+}
1953+
1954 main() {
1955 local short_opts="a:A:d:h:i:k:n:p:v"
1956 local long_opts="add:,append:,arch:,bios:,disk:,dowait,help,initrd:,kernel:,mem:,netdev:,no-dowait,power:,publish:,root-arg:,silent,serial-log:,uefi-nvram:,verbose,vnc:"
1957@@ -371,6 +470,8 @@
1958 # 3=src:size:driver
1959 # 4=src:size:driver:bsize
1960 # 5=src:size:driver:bsize:devopts
1961+ # 6=src:size:iscsi:bsize:target
1962+ # 7=src:size:iscsi:bsize:target:user:password:iuser:ipassword
1963 src=$(echo $disk | awk -F: '{print $1}')
1964 size=$(echo $disk | awk -F: '{print $2}')
1965 driver=$(echo $disk | awk -F: '{print $3}')
1966@@ -404,6 +505,40 @@
1967 { error "failed to determine format of $src"; return 1; }
1968 fi
1969
1970+ # We do not pass iSCSI disks down to qemu, as that will use
1971+ # qemu's iSCSI target layer and not the host tgt
1972+ if [ "${driver}" == "iscsi" ]; then
1973+ local target="" tid="" user="" password=""
1974+ local iuser="" ipassword=""
1975+ target=$(echo "$disk" | awk -F: '{print $5}') &&
1976+ [ -n "$target" ] || {
1977+ error "empty target for iSCSI disk '$disk'"
1978+ return 1
1979+ }
1980+ user=$(echo "$disk" | awk -F: '{print $6}')
1981+ password=$(echo "$disk" | awk -F: '{print $7}')
1982+ [ -n "$user" -a -n "$password" ] || \
1983+ [ -z "$user" -a -z "$password" ] || {
1984+ error "both target user ($user) and password ($password) " \
1985+ "must be specified for iSCSI disk '$disk'"
1986+ return 1
1987+ }
1988+ iuser=$(echo "$disk" | awk -F: '{print $8}')
1989+ ipassword=$(echo "$disk" | awk -F: '{print $9}')
1990+ [ -n "$iuser" -a -n "$ipassword" ] || \
1991+ [ -z "$iuser" -a -z "$ipassword" ] || {
1992+ error "both initiator user ($iuser) and password ($ipassword) " \
1993+ "must be specified for iSCSI disk '$disk'"
1994+ return 1
1995+ }
1996+ get_tgt_tid "$target" "$src" || return
1997+ tid="$_RET"
1998+ configure_tgt_auth "$tid" "$user" "$password" \
1999+ "$iuser" "$ipassword" || return
2000+ debug 1 "registered $disk to tgt tid=$tid"
2001+ continue
2002+ fi
2003+
2004 # prepend comma if passing devopts
2005 if [ -n "${devopts}" ]; then
2006 devopts=",${devopts}"
2007
2008=== modified file 'tools/vmtest-system-setup'
2009--- tools/vmtest-system-setup 2016-04-04 15:48:10 +0000
2010+++ tools/vmtest-system-setup 2017-02-22 16:43:05 +0000
2011@@ -21,6 +21,7 @@
2012 simplestreams
2013 $qemu
2014 ubuntu-cloudimage-keyring
2015+ tgt
2016 )
2017
2018 apt_get() {

Subscribers

People subscribed via source and target branches