Merge lp:~nacc/curtin/iscsi-wip into lp:~curtin-dev/curtin/trunk
- iscsi-wip
- Merge into trunk
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 |
Related bugs: |
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:/
path: iscsi:[
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-
Description of the change
Server Team CI bot (server-team-bot) wrote : | # |
- 454. By Nish Aravamudan
-
tools/launch: fix apt_proxy support
tests/vmtests/__init_ _.py: use util.subp
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:454
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
https:/
Executed test runs:
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 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
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:456
https:/
Executed test runs:
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 457. By Nish Aravamudan
-
overexuberant, -I for some reason is a literal string (not a = value)
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:457
https:/
Executed test runs:
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 458. By Nish Aravamudan
-
Fix flake and tox errors.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:458
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 459. By Nish Aravamudan
-
Drop unnecessary + for string concatenation.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:459
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
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.
- 460. By Nish Aravamudan
-
Merge from ~smoser/iscsi-wip
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:460
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Scott Moser (smoser) wrote : | # |
lots of little things.
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.
- 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
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:462
https:/
Executed test runs:
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 464. By Nish Aravamudan
-
fix launch further
- 465. By Nish Aravamudan
-
Fix tox
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:465
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 466. By Nish Aravamudan
-
merge with trunk
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:466
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 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
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:468
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 469. By Nish Aravamudan
-
Fix block/iscsi capture subp parameter usage.
Fix tools/launch which dropped ACL on the smoser merge.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:469
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 470. By Nish Aravamudan
-
tools/launch: revert empty apt proxy support (unrelated to iscsi)
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:470
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Scott Moser (smoser) wrote : | # |
save_iscsi_config:
^^^ sounds like a warning at least, maybe a raise exception?
tests/vmtests/
IPV4_
I suggested the urlparse in part to be more robust in parsing.
I quickly googled "valid ip6 address".
http://
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:/
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.
- 471. By Nish Aravamudan
-
Review updates -- unittests, str representation of disks.
- 472. By Nish Aravamudan
-
Merge with trunk
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:472
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Scott Moser (smoser) wrote : | # |
Nish,
unit tests look really good, thank you for that.
- 473. By Nish Aravamudan
-
Cleanup unit tests.
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:473
https:/
Executed test runs:
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 474. By Nish Aravamudan
-
Fix tox
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:474
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 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.
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:480
https:/
Executed test runs:
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:481
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 482. By Nish Aravamudan
-
basic auth support for iSCSI (untested)
- 483. By Nish Aravamudan
-
Successful vmtest with auth.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:483
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 484. By Nish Aravamudan
-
iSCSI documentation
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:484
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 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.
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:485
https:/
Executed test runs:
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 486. By Nish Aravamudan
-
Fix `make style-check`.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:486
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Scott Moser (smoser) wrote : | # |
We no longer have any callers of 'is_valid_
- 487. By Nish Aravamudan
-
Address smoser's review comments.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:487
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 488. By Nish Aravamudan
-
Updates for review comments
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:488
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 489. By Nish Aravamudan
-
Do not assume /dev/disk/by-path exists (fixes vmtests)
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:489
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 490. By Nish Aravamudan
-
Ensure every vmtest has a _iscsi_disks member (defaults to an empty
list). - 491. By Nish Aravamudan
-
Actually fix vmtests...
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:491
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 492. By Nish Aravamudan
-
Remove unused function.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:492
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 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.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:494
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Nish Aravamudan (nacc) wrote : | # |
Full vmtest run of r494: https:/
- 495. By Nish Aravamudan
-
Merge with trunk
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:495
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Nish Aravamudan (nacc) wrote : | # |
https:/
Passes all vmtests except for the known bcache change in the upstream kernel.
Preview Diff
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() { |
FAILED: Continuous integration, rev:453 /code.launchpad .net/~nacc/ curtin/ iscsi-wip/ +merge/ 316783/ +edit-commit- message
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
https:/ /jenkins. ubuntu. com/server/ job/curtin- ci/341/ /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= metal-arm64/ 341/console /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= metal-ppc64el/ 341/console /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= metal-s390x/ 341/console /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= vm-amd64/ 341/console /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= vm-i386/ 341/console
Executed test runs:
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild: /jenkins. ubuntu. com/server/ job/curtin- ci/341/ rebuild
https:/