Merge lp:~raharper/curtin/trunk.fix-iscsi-shutdown into lp:~curtin-dev/curtin/trunk

Proposed by Ryan Harper
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
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

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

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
524. By Ryan Harper

iscsi: disconnect prior to unmounting target

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

Check if target is in active session, else skip disconnect

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

Add unittests for iscsi.disconnect_target_disks

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

Don't rely on ephemeral env, use target iscsi config, disconnect before unmount

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :
Revision history for this message
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.

Revision history for this message
Ryan Harper (raharper) wrote :
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
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

Revision history for this message
Ryan Harper (raharper) wrote :

This branch ran through vmtest running iscsi tests (passes).

https://jenkins.ubuntu.com/server/view/curtin/job/curtin-vmtest-devel-amd64/38/console

Revision history for this message
Chad Smith (chad.smith) wrote :

Looks really good thanks for this (and all the unit tests give me warm fuzzies). +1

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

+1 on this thanks for the comments/discussion

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'curtin/block/iscsi.py'
--- curtin/block/iscsi.py 2017-05-18 23:54:21 +0000
+++ curtin/block/iscsi.py 2017-08-31 19:22:49 +0000
@@ -238,11 +238,35 @@
238 return _ISCSI_DISKS238 return _ISCSI_DISKS
239239
240240
241def get_iscsi_disks_from_config(cfg):
242 """Parse a curtin storage config and return a list
243 of iscsi disk objects for each configuration present
244 """
245 if not cfg:
246 cfg = {}
247
248 sconfig = cfg.get('storage', {}).get('config', {})
249 if not sconfig:
250 LOG.warning('Configuration dictionary did not contain'
251 ' a storage configuration')
252 return []
253
254 # Construct IscsiDisk objects for each iscsi volume present
255 iscsi_disks = [IscsiDisk(disk['path']) for disk in sconfig
256 if disk['type'] == 'disk' and
257 disk.get('path', "").startswith('iscsi:')]
258 LOG.debug('Found %s iscsi disks in storage config', len(iscsi_disks))
259 return iscsi_disks
260
261
241def disconnect_target_disks(target_root_path=None):262def disconnect_target_disks(target_root_path=None):
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')
243 fails = []264 fails = []
244 if os.path.isdir(target_nodes_path):265 if os.path.isdir(target_nodes_path):
245 for target in os.listdir(target_nodes_path):266 for target in os.listdir(target_nodes_path):
267 if target not in iscsiadm_sessions():
268 LOG.debug('iscsi target %s not active, skipping', target)
269 continue
246 # conn is "host,port,lun"270 # conn is "host,port,lun"
247 for conn in os.listdir(271 for conn in os.listdir(
248 os.path.sep.join([target_nodes_path, target])):272 os.path.sep.join([target_nodes_path, target])):
@@ -254,7 +278,9 @@
254 fails.append(target)278 fails.append(target)
255 LOG.warn("Unable to logout of iSCSI target %s: %s",279 LOG.warn("Unable to logout of iSCSI target %s: %s",
256 target, e)280 target, e)
257281 else:
282 LOG.warning('Skipping disconnect: failed to find iscsi nodes path: %s',
283 target_nodes_path)
258 if fails:284 if fails:
259 raise RuntimeError(285 raise RuntimeError(
260 "Unable to logout of iSCSI targets: %s" % ', '.join(fails))286 "Unable to logout of iSCSI targets: %s" % ', '.join(fails))
@@ -414,9 +440,15 @@
414440
415 def disconnect(self):441 def disconnect(self):
416 if self.target not in iscsiadm_sessions():442 if self.target not in iscsiadm_sessions():
443 LOG.warning('Iscsi target %s not in active iscsi sessions',
444 self.target)
417 return445 return
418446
419 util.subp(['sync'])447 try:
420 iscsiadm_logout(self.target, self.portal)448 util.subp(['sync'])
449 iscsiadm_logout(self.target, self.portal)
450 except util.ProcessExecutionError as e:
451 LOG.warn("Unable to logout of iSCSI target %s from portal %s: %s",
452 self.target, self.portal, e)
421453
422# vi: ts=4 expandtab syntax=python454# vi: ts=4 expandtab syntax=python
423455
=== modified file 'curtin/commands/install.py'
--- curtin/commands/install.py 2017-07-25 16:24:07 +0000
+++ curtin/commands/install.py 2017-08-31 19:22:49 +0000
@@ -477,9 +477,14 @@
477 '/root/curtin-install.log')477 '/root/curtin-install.log')
478 if log_target_path:478 if log_target_path:
479 copy_install_log(logfile, workingd.target, log_target_path)479 copy_install_log(logfile, workingd.target, log_target_path)
480 # unmount everything (including iscsi disks)
480 util.do_umount(workingd.target, recursive=True)481 util.do_umount(workingd.target, recursive=True)
481 # need to do some processing on iscsi disks to disconnect?482 # disconnect configured iscsi disks
482 iscsi.disconnect_target_disks(workingd.target)483 LOG.info("Disconnecting iscsi targets (if present)")
484 for iscsi_disk in iscsi.get_iscsi_disks_from_config(cfg):
485 LOG.info("Attempting to disconnect %s", iscsi_disk)
486 iscsi_disk.disconnect()
487
483 shutil.rmtree(workingd.top)488 shutil.rmtree(workingd.top)
484489
485 apply_power_state(cfg.get('power_state'))490 apply_power_state(cfg.get('power_state'))
486491
=== modified file 'tests/unittests/test_block_iscsi.py'
--- tests/unittests/test_block_iscsi.py 2017-08-03 18:14:51 +0000
+++ tests/unittests/test_block_iscsi.py 2017-08-31 19:22:49 +0000
@@ -1,6 +1,8 @@
1import mock1import mock
2import os
23
3from curtin.block import iscsi4from curtin.block import iscsi
5from curtin import util
4from .helpers import CiTestCase6from .helpers import CiTestCase
57
68
@@ -557,4 +559,183 @@
557 with self.assertRaises(ValueError):559 with self.assertRaises(ValueError):
558 iscsi.volpath_is_iscsi(None)560 iscsi.volpath_is_iscsi(None)
559561
562
563class TestBlockIscsiDiskFromConfig(CiTestCase):
564 # Test iscsi parsing of storage config for iscsi configure disks
565
566 def setUp(self):
567 super(TestBlockIscsiDiskFromConfig, self).setUp()
568 self.add_patch('curtin.block.iscsi.util.subp', 'mock_subp')
569
570 def test_parse_iscsi_disk_from_config(self):
571 """Test parsing iscsi volume path creates the same iscsi disk"""
572 target = 'curtin-659d5f45-4f23-46cb-b826-f2937b896e09'
573 iscsi_path = 'iscsi:10.245.168.20::20112:1:' + target
574 cfg = {
575 'storage': {
576 'config': [{'type': 'disk',
577 'id': 'iscsidev1',
578 'path': iscsi_path,
579 'name': 'iscsi_disk1',
580 'ptable': 'msdos',
581 'wipe': 'superblock'}]
582 }
583 }
584 expected_iscsi_disk = iscsi.IscsiDisk(iscsi_path)
585 iscsi_disk = iscsi.get_iscsi_disks_from_config(cfg).pop()
586 # utilize IscsiDisk str method for equality check
587 self.assertEqual(str(expected_iscsi_disk), str(iscsi_disk))
588
589 def test_parse_iscsi_disk_from_config_no_iscsi(self):
590 """Test parsing storage config with no iscsi disks included"""
591 cfg = {
592 'storage': {
593 'config': [{'type': 'disk',
594 'id': 'ssd1',
595 'path': 'dev/slash/foo1',
596 'name': 'the-fast-one',
597 'ptable': 'gpt',
598 'wipe': 'superblock'}]
599 }
600 }
601 expected_iscsi_disks = []
602 iscsi_disks = iscsi.get_iscsi_disks_from_config(cfg)
603 self.assertEqual(expected_iscsi_disks, iscsi_disks)
604
605 def test_parse_iscsi_disk_from_config_invalid_iscsi(self):
606 """Test parsing storage config with no iscsi disks included"""
607 cfg = {
608 'storage': {
609 'config': [{'type': 'disk',
610 'id': 'iscsidev2',
611 'path': 'iscsi:garbage',
612 'name': 'noob-city',
613 'ptable': 'msdos',
614 'wipe': 'superblock'}]
615 }
616 }
617 with self.assertRaises(ValueError):
618 iscsi.get_iscsi_disks_from_config(cfg)
619
620 def test_parse_iscsi_disk_from_config_empty(self):
621 """Test parse_iscsi_disks handles empty/invalid config"""
622 expected_iscsi_disks = []
623 iscsi_disks = iscsi.get_iscsi_disks_from_config({})
624 self.assertEqual(expected_iscsi_disks, iscsi_disks)
625
626 cfg = {'storage': {'config': []}}
627 iscsi_disks = iscsi.get_iscsi_disks_from_config(cfg)
628 self.assertEqual(expected_iscsi_disks, iscsi_disks)
629
630 def test_parse_iscsi_disk_from_config_none(self):
631 """Test parse_iscsi_disks handles no config"""
632 expected_iscsi_disks = []
633 iscsi_disks = iscsi.get_iscsi_disks_from_config({})
634 self.assertEqual(expected_iscsi_disks, iscsi_disks)
635
636 cfg = None
637 iscsi_disks = iscsi.get_iscsi_disks_from_config(cfg)
638 self.assertEqual(expected_iscsi_disks, iscsi_disks)
639
640
641class TestBlockIscsiDisconnect(CiTestCase):
642 # test that when disconnecting iscsi targets we
643 # check that the target has an active session before
644 # issuing a disconnect command
645
646 def setUp(self):
647 super(TestBlockIscsiDisconnect, self).setUp()
648 self.add_patch('curtin.block.iscsi.util.subp', 'mock_subp')
649 self.add_patch('curtin.block.iscsi.iscsiadm_sessions',
650 'mock_iscsi_sessions')
651 # fake target_root + iscsi nodes dir
652 self.target_path = self.tmp_dir()
653 self.iscsi_nodes = os.path.join(self.target_path, 'etc/iscsi/nodes')
654 util.ensure_dir(self.iscsi_nodes)
655
656 def _fmt_disconnect(self, target, portal):
657 return ['iscsiadm', '--mode=node', '--targetname=%s' % target,
658 '--portal=%s' % portal, '--logout']
659
660 def _setup_nodes(self, sessions, connection):
661 # setup iscsi_nodes dir (<fakeroot>/etc/iscsi/nodes) with content
662 for s in sessions:
663 sdir = os.path.join(self.iscsi_nodes, s)
664 connpath = os.path.join(sdir, connection)
665 util.ensure_dir(sdir)
666 util.write_file(connpath, content="")
667
668 def test_disconnect_target_disk(self):
669 """Test iscsi disconnecting multiple sessions, all present"""
670
671 sessions = [
672 'curtin-53ab23ff-a887-449a-80a8-288151208091',
673 'curtin-94b62de1-c579-42c0-879e-8a28178e64c5',
674 'curtin-556aeecd-a227-41b7-83d7-2bb471c574b4',
675 'curtin-fd0f644b-7858-420f-9997-3ea2aefe87b9'
676 ]
677 connection = '10.245.168.20,16395,1'
678 self._setup_nodes(sessions, connection)
679
680 self.mock_iscsi_sessions.return_value = "\n".join(sessions)
681
682 iscsi.disconnect_target_disks(self.target_path)
683
684 expected_calls = []
685 for session in sessions:
686 (host, port, _) = connection.split(',')
687 disconnect = self._fmt_disconnect(session, "%s:%s" % (host, port))
688 calls = [
689 mock.call(['sync']),
690 mock.call(disconnect, capture=True, log_captured=True),
691 mock.call(['udevadm', 'settle']),
692 ]
693 expected_calls.extend(calls)
694
695 self.mock_subp.assert_has_calls(expected_calls, any_order=True)
696
697 def test_disconnect_target_disk_skip_disconnected(self):
698 """Test iscsi does not attempt to disconnect already closed sessions"""
699 sessions = [
700 'curtin-53ab23ff-a887-449a-80a8-288151208091',
701 'curtin-94b62de1-c579-42c0-879e-8a28178e64c5',
702 'curtin-556aeecd-a227-41b7-83d7-2bb471c574b4',
703 'curtin-fd0f644b-7858-420f-9997-3ea2aefe87b9'
704 ]
705 connection = '10.245.168.20,16395,1'
706 self._setup_nodes(sessions, connection)
707 # Test with all sessions are already disconnected
708 self.mock_iscsi_sessions.return_value = ""
709
710 iscsi.disconnect_target_disks(self.target_path)
711
712 self.mock_subp.assert_has_calls([], any_order=True)
713
714 @mock.patch('curtin.block.iscsi.iscsiadm_logout')
715 def test_disconnect_target_disk_raises_runtime_error(self, mock_logout):
716 """Test iscsi raises RuntimeError if we fail to logout"""
717 sessions = [
718 'curtin-53ab23ff-a887-449a-80a8-288151208091',
719 ]
720 connection = '10.245.168.20,16395,1'
721 self._setup_nodes(sessions, connection)
722 self.mock_iscsi_sessions.return_value = "\n".join(sessions)
723 mock_logout.side_effect = util.ProcessExecutionError()
724
725 with self.assertRaises(RuntimeError):
726 iscsi.disconnect_target_disks(self.target_path)
727
728 expected_calls = []
729 for session in sessions:
730 (host, port, _) = connection.split(',')
731 disconnect = self._fmt_disconnect(session, "%s:%s" % (host, port))
732 calls = [
733 mock.call(['sync']),
734 mock.call(disconnect, capture=True, log_captured=True),
735 mock.call(['udevadm', 'settle']),
736 ]
737 expected_calls.extend(calls)
738
739 self.mock_subp.assert_has_calls([], any_order=True)
740
560# vi: ts=4 expandtab syntax=python741# vi: ts=4 expandtab syntax=python
561742
=== modified file 'tests/vmtests/test_lvm_iscsi.py'
--- tests/vmtests/test_lvm_iscsi.py 2017-08-02 15:46:35 +0000
+++ tests/vmtests/test_lvm_iscsi.py 2017-08-31 19:22:49 +0000
@@ -61,3 +61,7 @@
6161
62class ZestyTestIscsiLvm(relbase.zesty, TestLvmIscsiAbs):62class ZestyTestIscsiLvm(relbase.zesty, TestLvmIscsiAbs):
63 __test__ = True63 __test__ = True
64
65
66class ArtfulTestIscsiLvm(relbase.artful, TestLvmIscsiAbs):
67 __test__ = True

Subscribers

People subscribed via source and target branches