Merge lp:~raharper/curtin/trunk.fix-iscsi-shutdown into lp:~curtin-dev/curtin/trunk
- trunk.fix-iscsi-shutdown
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 523 |
Proposed branch: | lp:~raharper/curtin/trunk.fix-iscsi-shutdown |
Merge into: | lp:~curtin-dev/curtin/trunk |
Diff against target: |
298 lines (+227/-5) 4 files modified
curtin/block/iscsi.py (+35/-3) curtin/commands/install.py (+7/-2) tests/unittests/test_block_iscsi.py (+181/-0) tests/vmtests/test_lvm_iscsi.py (+4/-0) |
To merge this branch: | bzr merge lp:~raharper/curtin/trunk.fix-iscsi-shutdown |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
Chad Smith | Approve | ||
Nish Aravamudan | Pending | ||
Review via email: mp+329777@code.launchpad.net |
Commit message
Description of the change
iscsi: use curtin storage config to disconnect iscsi targets
When curtin disconnects from iscsi targets after an install, it does so
after unmounting the targets, however it passed the target dir into the
iscsi disconnect code which then silently failed to find any
session configurations.
We've been lucky as the existing shutdown iscsi service has been
logging out of sessions defined on the host; however on recent
Artful systems, the open-iscsi service is not restarted after
populating /etc/iscsi/nodes directory and since it is not running
in the ephemeral environment the service will not automatically
stop the iscsi devices, causing a hang on shutdown.
To resolve this parse the curtin storage config for iscsi disks,
construct IscsiDisk objects and then call their disconnect() method.
In addition to this logic fix, this branch contains:
- adds a missing Artful LVM Iscsi vmtest class
- Updated logging messages in iscsi paths, including
a message when iscsi_disconnect doesn't find the
/etc/iscsi/nodes directory.
- Updated IscsiDisk disconnect with similar logging
Server Team CI bot (server-team-bot) wrote : | # |
- 524. By Ryan Harper
-
iscsi: disconnect prior to unmounting target
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:524
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 525. By Ryan Harper
-
Check if target is in active session, else skip disconnect
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:525
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 526. By Ryan Harper
-
Add unittests for iscsi.disconnec
t_target_ disks
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:526
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 527. By Ryan Harper
-
Don't rely on ephemeral env, use target iscsi config, disconnect before unmount
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:527
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Ryan Harper (raharper) wrote : | # |
Kicking of vmtest of iscsi classes here:
https:/
Scott Moser (smoser) wrote : | # |
Nish,
could you think about this ?
the big change Ryan is making is moving disconnect to happen before unmount rather than after.
that would seem like it could be an issue to me.. if you disconnet a volume and then unmount it could reasonably expect to flush data to that volume.
- 528. By Ryan Harper
-
Switch back to unmounting all paths, and then disconnecting iscsi
In general, the typical cleanup of an in-use of block device involves
unmounting the filesystem/block device, and then perform any block
device specific actions. In this case, curtin will recursively unmount
any devices mounted under the install target directory (iscsi disks included)
And then we query the curtin iscsi layer for any connected iscsi devices it
created and invoke disconnect on each device. This should avoid shutting down
any other sessions which may be active but are not created/owned by curtin.In addition, add some logging to some of the iscsi paths which previously
would silently exit without performing the requested action (disconnect for
example returns if the target iscsi node dir is not present). - 529. By Ryan Harper
-
Don't use iscsi globals, not available across stages
The iscsi global is not available across stages due to use of
subprocess to run curtin commands. Revert back to finding
on-disk sessions that curtin configured to run iscsi disconnects.
Ryan Harper (raharper) wrote : | # |
Re-running vmtests on iscsi testcases here:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:529
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 530. By Ryan Harper
-
Parse storage config for reconstructing iscsi disk objects for disconnect operations
- 531. By Ryan Harper
-
config is a list
- 532. By Ryan Harper
-
Clean up logging messages
- 533. By Ryan Harper
-
Add unittest for parsing storage config for iscsi disks
Ryan Harper (raharper) wrote : | # |
This branch ran through vmtest running iscsi tests (passes).
https:/
Chad Smith (chad.smith) wrote : | # |
Looks really good thanks for this (and all the unit tests give me warm fuzzies). +1
Ryan Harper (raharper) : | # |
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:532
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Chad Smith (chad.smith) wrote : | # |
+1 on this thanks for the comments/discussion
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:533
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Preview Diff
1 | === modified file 'curtin/block/iscsi.py' | |||
2 | --- curtin/block/iscsi.py 2017-05-18 23:54:21 +0000 | |||
3 | +++ curtin/block/iscsi.py 2017-08-31 19:22:49 +0000 | |||
4 | @@ -238,11 +238,35 @@ | |||
5 | 238 | return _ISCSI_DISKS | 238 | return _ISCSI_DISKS |
6 | 239 | 239 | ||
7 | 240 | 240 | ||
8 | 241 | def get_iscsi_disks_from_config(cfg): | ||
9 | 242 | """Parse a curtin storage config and return a list | ||
10 | 243 | of iscsi disk objects for each configuration present | ||
11 | 244 | """ | ||
12 | 245 | if not cfg: | ||
13 | 246 | cfg = {} | ||
14 | 247 | |||
15 | 248 | sconfig = cfg.get('storage', {}).get('config', {}) | ||
16 | 249 | if not sconfig: | ||
17 | 250 | LOG.warning('Configuration dictionary did not contain' | ||
18 | 251 | ' a storage configuration') | ||
19 | 252 | return [] | ||
20 | 253 | |||
21 | 254 | # Construct IscsiDisk objects for each iscsi volume present | ||
22 | 255 | iscsi_disks = [IscsiDisk(disk['path']) for disk in sconfig | ||
23 | 256 | if disk['type'] == 'disk' and | ||
24 | 257 | disk.get('path', "").startswith('iscsi:')] | ||
25 | 258 | LOG.debug('Found %s iscsi disks in storage config', len(iscsi_disks)) | ||
26 | 259 | return iscsi_disks | ||
27 | 260 | |||
28 | 261 | |||
29 | 241 | def disconnect_target_disks(target_root_path=None): | 262 | def disconnect_target_disks(target_root_path=None): |
30 | 242 | target_nodes_path = util.target_path(target_root_path, '/etc/iscsi/nodes') | 263 | target_nodes_path = util.target_path(target_root_path, '/etc/iscsi/nodes') |
31 | 243 | fails = [] | 264 | fails = [] |
32 | 244 | if os.path.isdir(target_nodes_path): | 265 | if os.path.isdir(target_nodes_path): |
33 | 245 | for target in os.listdir(target_nodes_path): | 266 | for target in os.listdir(target_nodes_path): |
34 | 267 | if target not in iscsiadm_sessions(): | ||
35 | 268 | LOG.debug('iscsi target %s not active, skipping', target) | ||
36 | 269 | continue | ||
37 | 246 | # conn is "host,port,lun" | 270 | # conn is "host,port,lun" |
38 | 247 | for conn in os.listdir( | 271 | for conn in os.listdir( |
39 | 248 | os.path.sep.join([target_nodes_path, target])): | 272 | os.path.sep.join([target_nodes_path, target])): |
40 | @@ -254,7 +278,9 @@ | |||
41 | 254 | fails.append(target) | 278 | fails.append(target) |
42 | 255 | LOG.warn("Unable to logout of iSCSI target %s: %s", | 279 | LOG.warn("Unable to logout of iSCSI target %s: %s", |
43 | 256 | target, e) | 280 | target, e) |
45 | 257 | 281 | else: | |
46 | 282 | LOG.warning('Skipping disconnect: failed to find iscsi nodes path: %s', | ||
47 | 283 | target_nodes_path) | ||
48 | 258 | if fails: | 284 | if fails: |
49 | 259 | raise RuntimeError( | 285 | raise RuntimeError( |
50 | 260 | "Unable to logout of iSCSI targets: %s" % ', '.join(fails)) | 286 | "Unable to logout of iSCSI targets: %s" % ', '.join(fails)) |
51 | @@ -414,9 +440,15 @@ | |||
52 | 414 | 440 | ||
53 | 415 | def disconnect(self): | 441 | def disconnect(self): |
54 | 416 | if self.target not in iscsiadm_sessions(): | 442 | if self.target not in iscsiadm_sessions(): |
55 | 443 | LOG.warning('Iscsi target %s not in active iscsi sessions', | ||
56 | 444 | self.target) | ||
57 | 417 | return | 445 | return |
58 | 418 | 446 | ||
61 | 419 | util.subp(['sync']) | 447 | try: |
62 | 420 | iscsiadm_logout(self.target, self.portal) | 448 | util.subp(['sync']) |
63 | 449 | iscsiadm_logout(self.target, self.portal) | ||
64 | 450 | except util.ProcessExecutionError as e: | ||
65 | 451 | LOG.warn("Unable to logout of iSCSI target %s from portal %s: %s", | ||
66 | 452 | self.target, self.portal, e) | ||
67 | 421 | 453 | ||
68 | 422 | # vi: ts=4 expandtab syntax=python | 454 | # vi: ts=4 expandtab syntax=python |
69 | 423 | 455 | ||
70 | === modified file 'curtin/commands/install.py' | |||
71 | --- curtin/commands/install.py 2017-07-25 16:24:07 +0000 | |||
72 | +++ curtin/commands/install.py 2017-08-31 19:22:49 +0000 | |||
73 | @@ -477,9 +477,14 @@ | |||
74 | 477 | '/root/curtin-install.log') | 477 | '/root/curtin-install.log') |
75 | 478 | if log_target_path: | 478 | if log_target_path: |
76 | 479 | copy_install_log(logfile, workingd.target, log_target_path) | 479 | copy_install_log(logfile, workingd.target, log_target_path) |
77 | 480 | # unmount everything (including iscsi disks) | ||
78 | 480 | util.do_umount(workingd.target, recursive=True) | 481 | util.do_umount(workingd.target, recursive=True) |
81 | 481 | # need to do some processing on iscsi disks to disconnect? | 482 | # disconnect configured iscsi disks |
82 | 482 | iscsi.disconnect_target_disks(workingd.target) | 483 | LOG.info("Disconnecting iscsi targets (if present)") |
83 | 484 | for iscsi_disk in iscsi.get_iscsi_disks_from_config(cfg): | ||
84 | 485 | LOG.info("Attempting to disconnect %s", iscsi_disk) | ||
85 | 486 | iscsi_disk.disconnect() | ||
86 | 487 | |||
87 | 483 | shutil.rmtree(workingd.top) | 488 | shutil.rmtree(workingd.top) |
88 | 484 | 489 | ||
89 | 485 | apply_power_state(cfg.get('power_state')) | 490 | apply_power_state(cfg.get('power_state')) |
90 | 486 | 491 | ||
91 | === modified file 'tests/unittests/test_block_iscsi.py' | |||
92 | --- tests/unittests/test_block_iscsi.py 2017-08-03 18:14:51 +0000 | |||
93 | +++ tests/unittests/test_block_iscsi.py 2017-08-31 19:22:49 +0000 | |||
94 | @@ -1,6 +1,8 @@ | |||
95 | 1 | import mock | 1 | import mock |
96 | 2 | import os | ||
97 | 2 | 3 | ||
98 | 3 | from curtin.block import iscsi | 4 | from curtin.block import iscsi |
99 | 5 | from curtin import util | ||
100 | 4 | from .helpers import CiTestCase | 6 | from .helpers import CiTestCase |
101 | 5 | 7 | ||
102 | 6 | 8 | ||
103 | @@ -557,4 +559,183 @@ | |||
104 | 557 | with self.assertRaises(ValueError): | 559 | with self.assertRaises(ValueError): |
105 | 558 | iscsi.volpath_is_iscsi(None) | 560 | iscsi.volpath_is_iscsi(None) |
106 | 559 | 561 | ||
107 | 562 | |||
108 | 563 | class TestBlockIscsiDiskFromConfig(CiTestCase): | ||
109 | 564 | # Test iscsi parsing of storage config for iscsi configure disks | ||
110 | 565 | |||
111 | 566 | def setUp(self): | ||
112 | 567 | super(TestBlockIscsiDiskFromConfig, self).setUp() | ||
113 | 568 | self.add_patch('curtin.block.iscsi.util.subp', 'mock_subp') | ||
114 | 569 | |||
115 | 570 | def test_parse_iscsi_disk_from_config(self): | ||
116 | 571 | """Test parsing iscsi volume path creates the same iscsi disk""" | ||
117 | 572 | target = 'curtin-659d5f45-4f23-46cb-b826-f2937b896e09' | ||
118 | 573 | iscsi_path = 'iscsi:10.245.168.20::20112:1:' + target | ||
119 | 574 | cfg = { | ||
120 | 575 | 'storage': { | ||
121 | 576 | 'config': [{'type': 'disk', | ||
122 | 577 | 'id': 'iscsidev1', | ||
123 | 578 | 'path': iscsi_path, | ||
124 | 579 | 'name': 'iscsi_disk1', | ||
125 | 580 | 'ptable': 'msdos', | ||
126 | 581 | 'wipe': 'superblock'}] | ||
127 | 582 | } | ||
128 | 583 | } | ||
129 | 584 | expected_iscsi_disk = iscsi.IscsiDisk(iscsi_path) | ||
130 | 585 | iscsi_disk = iscsi.get_iscsi_disks_from_config(cfg).pop() | ||
131 | 586 | # utilize IscsiDisk str method for equality check | ||
132 | 587 | self.assertEqual(str(expected_iscsi_disk), str(iscsi_disk)) | ||
133 | 588 | |||
134 | 589 | def test_parse_iscsi_disk_from_config_no_iscsi(self): | ||
135 | 590 | """Test parsing storage config with no iscsi disks included""" | ||
136 | 591 | cfg = { | ||
137 | 592 | 'storage': { | ||
138 | 593 | 'config': [{'type': 'disk', | ||
139 | 594 | 'id': 'ssd1', | ||
140 | 595 | 'path': 'dev/slash/foo1', | ||
141 | 596 | 'name': 'the-fast-one', | ||
142 | 597 | 'ptable': 'gpt', | ||
143 | 598 | 'wipe': 'superblock'}] | ||
144 | 599 | } | ||
145 | 600 | } | ||
146 | 601 | expected_iscsi_disks = [] | ||
147 | 602 | iscsi_disks = iscsi.get_iscsi_disks_from_config(cfg) | ||
148 | 603 | self.assertEqual(expected_iscsi_disks, iscsi_disks) | ||
149 | 604 | |||
150 | 605 | def test_parse_iscsi_disk_from_config_invalid_iscsi(self): | ||
151 | 606 | """Test parsing storage config with no iscsi disks included""" | ||
152 | 607 | cfg = { | ||
153 | 608 | 'storage': { | ||
154 | 609 | 'config': [{'type': 'disk', | ||
155 | 610 | 'id': 'iscsidev2', | ||
156 | 611 | 'path': 'iscsi:garbage', | ||
157 | 612 | 'name': 'noob-city', | ||
158 | 613 | 'ptable': 'msdos', | ||
159 | 614 | 'wipe': 'superblock'}] | ||
160 | 615 | } | ||
161 | 616 | } | ||
162 | 617 | with self.assertRaises(ValueError): | ||
163 | 618 | iscsi.get_iscsi_disks_from_config(cfg) | ||
164 | 619 | |||
165 | 620 | def test_parse_iscsi_disk_from_config_empty(self): | ||
166 | 621 | """Test parse_iscsi_disks handles empty/invalid config""" | ||
167 | 622 | expected_iscsi_disks = [] | ||
168 | 623 | iscsi_disks = iscsi.get_iscsi_disks_from_config({}) | ||
169 | 624 | self.assertEqual(expected_iscsi_disks, iscsi_disks) | ||
170 | 625 | |||
171 | 626 | cfg = {'storage': {'config': []}} | ||
172 | 627 | iscsi_disks = iscsi.get_iscsi_disks_from_config(cfg) | ||
173 | 628 | self.assertEqual(expected_iscsi_disks, iscsi_disks) | ||
174 | 629 | |||
175 | 630 | def test_parse_iscsi_disk_from_config_none(self): | ||
176 | 631 | """Test parse_iscsi_disks handles no config""" | ||
177 | 632 | expected_iscsi_disks = [] | ||
178 | 633 | iscsi_disks = iscsi.get_iscsi_disks_from_config({}) | ||
179 | 634 | self.assertEqual(expected_iscsi_disks, iscsi_disks) | ||
180 | 635 | |||
181 | 636 | cfg = None | ||
182 | 637 | iscsi_disks = iscsi.get_iscsi_disks_from_config(cfg) | ||
183 | 638 | self.assertEqual(expected_iscsi_disks, iscsi_disks) | ||
184 | 639 | |||
185 | 640 | |||
186 | 641 | class TestBlockIscsiDisconnect(CiTestCase): | ||
187 | 642 | # test that when disconnecting iscsi targets we | ||
188 | 643 | # check that the target has an active session before | ||
189 | 644 | # issuing a disconnect command | ||
190 | 645 | |||
191 | 646 | def setUp(self): | ||
192 | 647 | super(TestBlockIscsiDisconnect, self).setUp() | ||
193 | 648 | self.add_patch('curtin.block.iscsi.util.subp', 'mock_subp') | ||
194 | 649 | self.add_patch('curtin.block.iscsi.iscsiadm_sessions', | ||
195 | 650 | 'mock_iscsi_sessions') | ||
196 | 651 | # fake target_root + iscsi nodes dir | ||
197 | 652 | self.target_path = self.tmp_dir() | ||
198 | 653 | self.iscsi_nodes = os.path.join(self.target_path, 'etc/iscsi/nodes') | ||
199 | 654 | util.ensure_dir(self.iscsi_nodes) | ||
200 | 655 | |||
201 | 656 | def _fmt_disconnect(self, target, portal): | ||
202 | 657 | return ['iscsiadm', '--mode=node', '--targetname=%s' % target, | ||
203 | 658 | '--portal=%s' % portal, '--logout'] | ||
204 | 659 | |||
205 | 660 | def _setup_nodes(self, sessions, connection): | ||
206 | 661 | # setup iscsi_nodes dir (<fakeroot>/etc/iscsi/nodes) with content | ||
207 | 662 | for s in sessions: | ||
208 | 663 | sdir = os.path.join(self.iscsi_nodes, s) | ||
209 | 664 | connpath = os.path.join(sdir, connection) | ||
210 | 665 | util.ensure_dir(sdir) | ||
211 | 666 | util.write_file(connpath, content="") | ||
212 | 667 | |||
213 | 668 | def test_disconnect_target_disk(self): | ||
214 | 669 | """Test iscsi disconnecting multiple sessions, all present""" | ||
215 | 670 | |||
216 | 671 | sessions = [ | ||
217 | 672 | 'curtin-53ab23ff-a887-449a-80a8-288151208091', | ||
218 | 673 | 'curtin-94b62de1-c579-42c0-879e-8a28178e64c5', | ||
219 | 674 | 'curtin-556aeecd-a227-41b7-83d7-2bb471c574b4', | ||
220 | 675 | 'curtin-fd0f644b-7858-420f-9997-3ea2aefe87b9' | ||
221 | 676 | ] | ||
222 | 677 | connection = '10.245.168.20,16395,1' | ||
223 | 678 | self._setup_nodes(sessions, connection) | ||
224 | 679 | |||
225 | 680 | self.mock_iscsi_sessions.return_value = "\n".join(sessions) | ||
226 | 681 | |||
227 | 682 | iscsi.disconnect_target_disks(self.target_path) | ||
228 | 683 | |||
229 | 684 | expected_calls = [] | ||
230 | 685 | for session in sessions: | ||
231 | 686 | (host, port, _) = connection.split(',') | ||
232 | 687 | disconnect = self._fmt_disconnect(session, "%s:%s" % (host, port)) | ||
233 | 688 | calls = [ | ||
234 | 689 | mock.call(['sync']), | ||
235 | 690 | mock.call(disconnect, capture=True, log_captured=True), | ||
236 | 691 | mock.call(['udevadm', 'settle']), | ||
237 | 692 | ] | ||
238 | 693 | expected_calls.extend(calls) | ||
239 | 694 | |||
240 | 695 | self.mock_subp.assert_has_calls(expected_calls, any_order=True) | ||
241 | 696 | |||
242 | 697 | def test_disconnect_target_disk_skip_disconnected(self): | ||
243 | 698 | """Test iscsi does not attempt to disconnect already closed sessions""" | ||
244 | 699 | sessions = [ | ||
245 | 700 | 'curtin-53ab23ff-a887-449a-80a8-288151208091', | ||
246 | 701 | 'curtin-94b62de1-c579-42c0-879e-8a28178e64c5', | ||
247 | 702 | 'curtin-556aeecd-a227-41b7-83d7-2bb471c574b4', | ||
248 | 703 | 'curtin-fd0f644b-7858-420f-9997-3ea2aefe87b9' | ||
249 | 704 | ] | ||
250 | 705 | connection = '10.245.168.20,16395,1' | ||
251 | 706 | self._setup_nodes(sessions, connection) | ||
252 | 707 | # Test with all sessions are already disconnected | ||
253 | 708 | self.mock_iscsi_sessions.return_value = "" | ||
254 | 709 | |||
255 | 710 | iscsi.disconnect_target_disks(self.target_path) | ||
256 | 711 | |||
257 | 712 | self.mock_subp.assert_has_calls([], any_order=True) | ||
258 | 713 | |||
259 | 714 | @mock.patch('curtin.block.iscsi.iscsiadm_logout') | ||
260 | 715 | def test_disconnect_target_disk_raises_runtime_error(self, mock_logout): | ||
261 | 716 | """Test iscsi raises RuntimeError if we fail to logout""" | ||
262 | 717 | sessions = [ | ||
263 | 718 | 'curtin-53ab23ff-a887-449a-80a8-288151208091', | ||
264 | 719 | ] | ||
265 | 720 | connection = '10.245.168.20,16395,1' | ||
266 | 721 | self._setup_nodes(sessions, connection) | ||
267 | 722 | self.mock_iscsi_sessions.return_value = "\n".join(sessions) | ||
268 | 723 | mock_logout.side_effect = util.ProcessExecutionError() | ||
269 | 724 | |||
270 | 725 | with self.assertRaises(RuntimeError): | ||
271 | 726 | iscsi.disconnect_target_disks(self.target_path) | ||
272 | 727 | |||
273 | 728 | expected_calls = [] | ||
274 | 729 | for session in sessions: | ||
275 | 730 | (host, port, _) = connection.split(',') | ||
276 | 731 | disconnect = self._fmt_disconnect(session, "%s:%s" % (host, port)) | ||
277 | 732 | calls = [ | ||
278 | 733 | mock.call(['sync']), | ||
279 | 734 | mock.call(disconnect, capture=True, log_captured=True), | ||
280 | 735 | mock.call(['udevadm', 'settle']), | ||
281 | 736 | ] | ||
282 | 737 | expected_calls.extend(calls) | ||
283 | 738 | |||
284 | 739 | self.mock_subp.assert_has_calls([], any_order=True) | ||
285 | 740 | |||
286 | 560 | # vi: ts=4 expandtab syntax=python | 741 | # vi: ts=4 expandtab syntax=python |
287 | 561 | 742 | ||
288 | === modified file 'tests/vmtests/test_lvm_iscsi.py' | |||
289 | --- tests/vmtests/test_lvm_iscsi.py 2017-08-02 15:46:35 +0000 | |||
290 | +++ tests/vmtests/test_lvm_iscsi.py 2017-08-31 19:22:49 +0000 | |||
291 | @@ -61,3 +61,7 @@ | |||
292 | 61 | 61 | ||
293 | 62 | class ZestyTestIscsiLvm(relbase.zesty, TestLvmIscsiAbs): | 62 | class ZestyTestIscsiLvm(relbase.zesty, TestLvmIscsiAbs): |
294 | 63 | __test__ = True | 63 | __test__ = True |
295 | 64 | |||
296 | 65 | |||
297 | 66 | class ArtfulTestIscsiLvm(relbase.artful, TestLvmIscsiAbs): | ||
298 | 67 | __test__ = True |
PASSED: Continuous integration, rev:523 /jenkins. ubuntu. com/server/ job/curtin- ci/600/ /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= metal-amd64/ 600 /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= metal-arm64/ 600 /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= metal-ppc64el/ 600 /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= metal-s390x/ 600
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild: /jenkins. ubuntu. com/server/ job/curtin- ci/600/ rebuild
https:/