Merge ~raharper/curtin:fix/mpath-partition-wiping into curtin:master

Proposed by Ryan Harper
Status: Merged
Approved by: Dan Watkins
Approved revision: 6a07152bfbed01ea2589320128d431de543e5baa
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~raharper/curtin:fix/mpath-partition-wiping
Merge into: curtin:master
Diff against target: 640 lines (+236/-25)
6 files modified
curtin/block/clear_holders.py (+32/-11)
curtin/block/multipath.py (+74/-2)
examples/tests/dirty_disks_config.yaml (+8/-0)
tests/unittests/test_block_multipath.py (+87/-6)
tests/unittests/test_clear_holders.py (+34/-6)
tests/vmtests/test_multipath.py (+1/-0)
Reviewer Review Type Date Requested Status
Dan Watkins (community) Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+377927@code.launchpad.net

Commit message

multipath: handle removal of multipath partitions correctly

Curtin could handle multipath-partition removal if it was given
the device-mapper device (dm-x) however, if it was fed the
underying scsi device (e.g. sda2) this would fail when curtin
would try to clear the partition requesting exclusive access
which it could not obtain due to the device-mapper device.

This branch implements a kernel scsi device lookup to map
sdXN -> dm-N mapping which resolves the issue by allowing
curtin to wipe the dm-X device even if it has been
provided the sdXN device.

Additional Changes
 - Enable dirty_disk mode on multipath test to reproduce
   failure

LP: #1857042

To post a comment you must log in.
5514b58... by Ryan Harper

Handle multipath not installed in dirty_disk setup.

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

A few inline comments/questions. Thanks!

cddfb4d... by Ryan Harper

Update comment and remove unneeded variable initialization.

79dd492... by Ryan Harper

Refactor logic to drop unneeded mpath_is_member call

10b2611... by Ryan Harper

multipath: move up no device found check

79748dd... by Ryan Harper

Handle dmsetup names with spaces

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

Working on fixes, thanks for the review, update coming in just a bit

36cdc45... by Ryan Harper

Add an additional path to make test more clear.

9f40323... by Ryan Harper

Use assertIsNone

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
1acb477... by Ryan Harper

multipath: add docstrings to unittests

Also, update the negative case for dmsetup ls output.

4b17684... by Ryan Harper

multipath: update docstrings and add supported check

Bionic and older systems do not have multipath installed by
default so curtin will check if multipath tools are present
and only issues multipath commands if they are needed. This
fixes the bionic basic test on this branch.

dd507cc... by Ryan Harper

Add footer to multipath.py

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

multipath: try harder when removing multipath devices, add verbosity

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

LGTM, thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/curtin/block/clear_holders.py b/curtin/block/clear_holders.py
index 10e7dc5..08aa70a 100644
--- a/curtin/block/clear_holders.py
+++ b/curtin/block/clear_holders.py
@@ -285,7 +285,25 @@ def wipe_superblock(device):
285 else:285 else:
286 bcache._stop_device(stop_path)286 bcache._stop_device(stop_path)
287287
288 _wipe_superblock(blockdev)288 # the blockdev (e.g. /dev/sda2) may be a multipath partition which can
289 # only be wiped via its device mapper device (e.g. /dev/dm-4)
290 # check for this and determine the correct device mapper value to use.
291 mp_dev = None
292 mp_support = multipath.multipath_supported()
293 if mp_support:
294 parent, partnum = block.get_blockdev_for_partition(blockdev)
295 parent_mpath_id = multipath.find_mpath_id_by_path(parent)
296 if parent_mpath_id is not None:
297 # construct multipath dmsetup id
298 # <mpathid>-part%d -> /dev/dm-1
299 mp_id, mp_dev = multipath.find_mpath_id_by_parent(parent_mpath_id,
300 partnum=partnum)
301 # if we don't find a mapping then the mp partition has already been
302 # wiped/removed
303 if mp_dev:
304 _wipe_superblock(mp_dev)
305 else:
306 _wipe_superblock(blockdev)
289307
290 # if we had partitions, make sure they've been removed308 # if we had partitions, make sure they've been removed
291 if partitions:309 if partitions:
@@ -308,16 +326,19 @@ def wipe_superblock(device):
308 device, attempt + 1, len(retries), wait)326 device, attempt + 1, len(retries), wait)
309 time.sleep(wait)327 time.sleep(wait)
310328
311 # multipath partitions are separate block devices (disks)329 if mp_support:
312 if multipath.is_mpath_partition(blockdev):330 # multipath partitions are separate block devices (disks)
313 multipath.remove_partition(blockdev)331 if mp_dev or multipath.is_mpath_partition(blockdev):
314 # multipath devices must be hidden to utilize a single member (path)332 multipath.remove_partition(mp_dev if mp_dev else blockdev)
315 elif multipath.is_mpath_device(blockdev):333 # multipath devices must be hidden to utilize a single member (path)
316 mp_id = multipath.find_mpath_id(blockdev)334 elif multipath.is_mpath_device(blockdev):
317 if mp_id:335 mp_id = multipath.find_mpath_id(blockdev)
318 multipath.remove_map(mp_id)336 multipath.remove_partition(blockdev)
319 else:337 if mp_id:
320 raise RuntimeError('Failed to find multipath id for %s' % blockdev)338 multipath.remove_map(mp_id)
339 else:
340 raise RuntimeError(
341 'Failed to find multipath id for %s' % blockdev)
321342
322343
323def _wipe_superblock(blockdev, exclusive=True, strict=True):344def _wipe_superblock(blockdev, exclusive=True, strict=True):
diff --git a/curtin/block/multipath.py b/curtin/block/multipath.py
index d1e8441..8ce0509 100644
--- a/curtin/block/multipath.py
+++ b/curtin/block/multipath.py
@@ -11,6 +11,7 @@ SHOW_MAPS_FMT = "name=%n multipath='%w' sysfs='%d' paths='%N'"
1111
1212
13def _extract_mpath_data(cmd, show_verb):13def _extract_mpath_data(cmd, show_verb):
14 """ Parse output from specifed command output via load_shell_content."""
14 data, _err = util.subp(cmd, capture=True)15 data, _err = util.subp(cmd, capture=True)
15 result = []16 result = []
16 for line in data.splitlines():17 for line in data.splitlines():
@@ -23,16 +24,34 @@ def _extract_mpath_data(cmd, show_verb):
2324
2425
25def show_paths():26def show_paths():
27 """ Query multipathd for paths output and return a dict of the values."""
26 cmd = ['multipathd', 'show', 'paths', 'raw', 'format', SHOW_PATHS_FMT]28 cmd = ['multipathd', 'show', 'paths', 'raw', 'format', SHOW_PATHS_FMT]
27 return _extract_mpath_data(cmd, 'paths')29 return _extract_mpath_data(cmd, 'paths')
2830
2931
30def show_maps():32def show_maps():
33 """ Query multipathd for maps output and return a dict of the values."""
31 cmd = ['multipathd', 'show', 'maps', 'raw', 'format', SHOW_MAPS_FMT]34 cmd = ['multipathd', 'show', 'maps', 'raw', 'format', SHOW_MAPS_FMT]
32 return _extract_mpath_data(cmd, 'maps')35 return _extract_mpath_data(cmd, 'maps')
3336
3437
38def dmname_to_blkdev_mapping():
39 """ Use dmsetup ls output to build a dict of DM_NAME, /dev/dm-x values."""
40 data, _err = util.subp(['dmsetup', 'ls', '-o', 'blkdevname'], capture=True)
41 mapping = {}
42 if data and data.strip() != "No devices found":
43 LOG.debug('multipath: dmsetup ls output:\n%s', data)
44 for line in data.splitlines():
45 if line:
46 dm_name, blkdev = line.split('\t')
47 # (dm-1) -> /dev/dm-1
48 mapping[dm_name] = '/dev/' + blkdev.strip('()')
49
50 return mapping
51
52
35def is_mpath_device(devpath):53def is_mpath_device(devpath):
54 """ Check if devpath is a multipath device, returns boolean. """
36 info = udev.udevadm_info(devpath)55 info = udev.udevadm_info(devpath)
37 if info.get('DM_UUID', '').startswith('mpath-'):56 if info.get('DM_UUID', '').startswith('mpath-'):
38 return True57 return True
@@ -41,6 +60,7 @@ def is_mpath_device(devpath):
4160
4261
43def is_mpath_member(devpath):62def is_mpath_member(devpath):
63 """ Check if a device is a multipath member (a path), returns boolean. """
44 try:64 try:
45 util.subp(['multipath', '-c', devpath], capture=True)65 util.subp(['multipath', '-c', devpath], capture=True)
46 return True66 return True
@@ -49,6 +69,7 @@ def is_mpath_member(devpath):
4969
5070
51def is_mpath_partition(devpath):71def is_mpath_partition(devpath):
72 """ Check if a device is a multipath partition, returns boolean. """
52 if devpath.startswith('/dev/dm-'):73 if devpath.startswith('/dev/dm-'):
53 if 'DM_PART' in udev.udevadm_info(devpath):74 if 'DM_PART' in udev.udevadm_info(devpath):
54 LOG.debug("%s is multipath device partition", devpath)75 LOG.debug("%s is multipath device partition", devpath)
@@ -58,6 +79,7 @@ def is_mpath_partition(devpath):
5879
5980
60def mpath_partition_to_mpath_id(devpath):81def mpath_partition_to_mpath_id(devpath):
82 """ Return the mpath id of a multipath partition. """
61 info = udev.udevadm_info(devpath)83 info = udev.udevadm_info(devpath)
62 if 'DM_MPATH' in info:84 if 'DM_MPATH' in info:
63 return info['DM_MPATH']85 return info['DM_MPATH']
@@ -66,9 +88,10 @@ def mpath_partition_to_mpath_id(devpath):
6688
6789
68def remove_partition(devpath, retries=10):90def remove_partition(devpath, retries=10):
91 """ Remove a multipath partition mapping. """
69 LOG.debug('multipath: removing multipath partition: %s', devpath)92 LOG.debug('multipath: removing multipath partition: %s', devpath)
70 for _ in range(0, retries):93 for _ in range(0, retries):
71 util.subp(['dmsetup', 'remove', devpath], rcs=[0, 1])94 util.subp(['dmsetup', 'remove', '--force', '--retry', devpath])
72 udev.udevadm_settle()95 udev.udevadm_settle()
73 if not os.path.exists(devpath):96 if not os.path.exists(devpath):
74 return97 return
@@ -77,10 +100,11 @@ def remove_partition(devpath, retries=10):
77100
78101
79def remove_map(map_id, retries=10):102def remove_map(map_id, retries=10):
103 """ Remove a multipath device mapping. """
80 LOG.debug('multipath: removing multipath map: %s', map_id)104 LOG.debug('multipath: removing multipath map: %s', map_id)
81 devpath = '/dev/mapper/%s' % map_id105 devpath = '/dev/mapper/%s' % map_id
82 for _ in range(0, retries):106 for _ in range(0, retries):
83 util.subp(['multipath', '-f', map_id], rcs=[0, 1])107 util.subp(['multipath', '-v3', '-R3', '-f', map_id], rcs=[0, 1])
84 udev.udevadm_settle()108 udev.udevadm_settle()
85 if not os.path.exists(devpath):109 if not os.path.exists(devpath):
86 return110 return
@@ -89,6 +113,7 @@ def remove_map(map_id, retries=10):
89113
90114
91def find_mpath_members(multipath_id, paths=None):115def find_mpath_members(multipath_id, paths=None):
116 """ Return a list of device path for each member of aspecified mpath_id."""
92 if not paths:117 if not paths:
93 paths = show_paths()118 paths = show_paths()
94119
@@ -98,6 +123,7 @@ def find_mpath_members(multipath_id, paths=None):
98123
99124
100def find_mpath_id(devpath, maps=None):125def find_mpath_id(devpath, maps=None):
126 """ Return the mpath_id associated with a specified device path. """
101 if not maps:127 if not maps:
102 maps = show_maps()128 maps = show_maps()
103129
@@ -109,3 +135,49 @@ def find_mpath_id(devpath, maps=None):
109 return mpmap['multipath']135 return mpmap['multipath']
110136
111 return None137 return None
138
139
140def find_mpath_id_by_path(devpath, paths=None):
141 """ Return the mpath_id associated with a specified device path. """
142 if not paths:
143 paths = show_paths()
144
145 for path in paths:
146 if devpath == '/dev/' + path['device']:
147 return path['multipath']
148
149 return None
150
151
152def find_mpath_id_by_parent(multipath_id, partnum=None):
153 """ Return the mpath_id associated with a specified device path. """
154 devmap = dmname_to_blkdev_mapping()
155 LOG.debug('multipath: dm_name blk map: %s', devmap)
156 dm_name = multipath_id
157 if partnum:
158 dm_name += "-part%d" % int(partnum)
159
160 return (dm_name, devmap.get(dm_name))
161
162
163def multipath_supported():
164 """Return a boolean indicating if multipath is supported."""
165 try:
166 multipath_assert_supported()
167 return True
168 except RuntimeError:
169 return False
170
171
172def multipath_assert_supported():
173 """ Determine if the runtime system supports multipath.
174 returns: True if system supports multipath
175 raises: RuntimeError: if system does not support multipath
176 """
177 missing_progs = [p for p in ('multipath', 'multipathd')
178 if not util.which(p)]
179 if missing_progs:
180 raise RuntimeError(
181 "Missing multipath utils: %s" % ','.join(missing_progs))
182
183# vi: ts=4 expandtab syntax=python
diff --git a/examples/tests/dirty_disks_config.yaml b/examples/tests/dirty_disks_config.yaml
index 9fd68ab..2f86f9e 100644
--- a/examples/tests/dirty_disks_config.yaml
+++ b/examples/tests/dirty_disks_config.yaml
@@ -22,6 +22,13 @@ bucket:
22 done22 done
23 swapon --show23 swapon --show
24 exit 024 exit 0
25 - &multipath_on |
26 #!/bin/sh
27 if command -v multipath; then
28 multipath -ll
29 multipath -r
30 multipath -ll
31 fi
25 - &zpool_export |32 - &zpool_export |
26 #!/bin/sh33 #!/bin/sh
27 # disable any rpools to trigger disks with zfs_member label but inactive34 # disable any rpools to trigger disks with zfs_member label but inactive
@@ -80,6 +87,7 @@ early_commands:
80 WORKING_DIR=/tmp/my.bdir/work.d,87 WORKING_DIR=/tmp/my.bdir/work.d,
81 curtin, --showtrace, -v, block-meta, --umount, custom]88 curtin, --showtrace, -v, block-meta, --umount, custom]
82 02-enable_swaps: [sh, -c, *swapon]89 02-enable_swaps: [sh, -c, *swapon]
90 02-multipath_on: [sh, -c, *multipath_on]
83 03-disable_rpool: [sh, -c, *zpool_export]91 03-disable_rpool: [sh, -c, *zpool_export]
84 04-lvm_stop: [sh, -c, *lvm_stop]92 04-lvm_stop: [sh, -c, *lvm_stop]
85 05-mdadm_stop: [sh, -c, *mdadm_stop]93 05-mdadm_stop: [sh, -c, *mdadm_stop]
diff --git a/tests/unittests/test_block_multipath.py b/tests/unittests/test_block_multipath.py
index 0964349..b0a8e32 100644
--- a/tests/unittests/test_block_multipath.py
+++ b/tests/unittests/test_block_multipath.py
@@ -4,6 +4,14 @@ from curtin.block import multipath
4from .helpers import CiTestCase, raise_pexec_error4from .helpers import CiTestCase, raise_pexec_error
55
66
7# dmsetup uses tabs as separators
8DMSETUP_LS_BLKDEV_OUTPUT = '''\
9mpatha (dm-0)
10mpatha-part1 (dm-1)
111gb zero (dm-2)
12'''
13
14
7class TestMultipath(CiTestCase):15class TestMultipath(CiTestCase):
816
9 def setUp(self):17 def setUp(self):
@@ -14,6 +22,7 @@ class TestMultipath(CiTestCase):
14 self.m_subp.return_value = ("", "")22 self.m_subp.return_value = ("", "")
1523
16 def test_show_paths(self):24 def test_show_paths(self):
25 """verify show_paths extracts mulitpath path data correctly."""
17 self.m_subp.return_value = ("foo=bar wark=2", "")26 self.m_subp.return_value = ("foo=bar wark=2", "")
18 expected = ['multipathd', 'show', 'paths', 'raw', 'format',27 expected = ['multipathd', 'show', 'paths', 'raw', 'format',
19 multipath.SHOW_PATHS_FMT]28 multipath.SHOW_PATHS_FMT]
@@ -22,6 +31,7 @@ class TestMultipath(CiTestCase):
22 self.m_subp.assert_called_with(expected, capture=True)31 self.m_subp.assert_called_with(expected, capture=True)
2332
24 def test_show_maps(self):33 def test_show_maps(self):
34 """verify show_maps extracts mulitpath map data correctly."""
25 self.m_subp.return_value = ("foo=bar wark=2", "")35 self.m_subp.return_value = ("foo=bar wark=2", "")
26 expected = ['multipathd', 'show', 'maps', 'raw', 'format',36 expected = ['multipathd', 'show', 'maps', 'raw', 'format',
27 multipath.SHOW_MAPS_FMT]37 multipath.SHOW_MAPS_FMT]
@@ -30,29 +40,36 @@ class TestMultipath(CiTestCase):
30 self.m_subp.assert_called_with(expected, capture=True)40 self.m_subp.assert_called_with(expected, capture=True)
3141
32 def test_is_mpath_device_true(self):42 def test_is_mpath_device_true(self):
43 """is_mpath_device returns true if dev DM_UUID starts with mpath-"""
33 self.m_udev.udevadm_info.return_value = {'DM_UUID': 'mpath-mpatha-foo'}44 self.m_udev.udevadm_info.return_value = {'DM_UUID': 'mpath-mpatha-foo'}
34 self.assertTrue(multipath.is_mpath_device(self.random_string()))45 self.assertTrue(multipath.is_mpath_device(self.random_string()))
3546
36 def test_is_mpath_device_false(self):47 def test_is_mpath_device_false(self):
48 """is_mpath_device returns false when DM_UUID doesnt start w/ mpath-"""
37 self.m_udev.udevadm_info.return_value = {'DM_UUID': 'lvm-vg-foo-lv1'}49 self.m_udev.udevadm_info.return_value = {'DM_UUID': 'lvm-vg-foo-lv1'}
38 self.assertFalse(multipath.is_mpath_device(self.random_string()))50 self.assertFalse(multipath.is_mpath_device(self.random_string()))
3951
40 def test_is_mpath_member_true(self):52 def test_is_mpath_member_true(self):
53 """is_mpath_device returns false when DM_UUID doesnt start w/ mpath-"""
41 self.assertTrue(multipath.is_mpath_member(self.random_string()))54 self.assertTrue(multipath.is_mpath_member(self.random_string()))
4255
43 def test_is_mpath_member_false(self):56 def test_is_mpath_member_false(self):
57 """is_mpath_member returns false if 'multipath -c <dev>' exits err."""
44 self.m_subp.side_effect = raise_pexec_error58 self.m_subp.side_effect = raise_pexec_error
45 self.assertFalse(multipath.is_mpath_member(self.random_string()))59 self.assertFalse(multipath.is_mpath_member(self.random_string()))
4660
47 def test_is_mpath_partition_true(self):61 def test_is_mpath_partition_true(self):
62 """is_mpath_member returns true if 'multipath -c <dev>' exits 0."""
48 dm_device = "/dev/dm-" + self.random_string()63 dm_device = "/dev/dm-" + self.random_string()
49 self.m_udev.udevadm_info.return_value = {'DM_PART': '1'}64 self.m_udev.udevadm_info.return_value = {'DM_PART': '1'}
50 self.assertTrue(multipath.is_mpath_partition(dm_device))65 self.assertTrue(multipath.is_mpath_partition(dm_device))
5166
52 def test_is_mpath_partition_false(self):67 def test_is_mpath_partition_false(self):
68 """is_mpath_member returns false if DM_PART is not present for dev."""
53 self.assertFalse(multipath.is_mpath_partition(self.random_string()))69 self.assertFalse(multipath.is_mpath_partition(self.random_string()))
5470
55 def test_mpath_partition_to_mpath_id(self):71 def test_mpath_partition_to_mpath_id(self):
72 """mpath_part_to_mpath_id extracts MD_MPATH value from mp partition."""
56 dev = self.random_string()73 dev = self.random_string()
57 mpath_id = self.random_string()74 mpath_id = self.random_string()
58 self.m_udev.udevadm_info.return_value = {'DM_MPATH': mpath_id}75 self.m_udev.udevadm_info.return_value = {'DM_MPATH': mpath_id}
@@ -60,18 +77,20 @@ class TestMultipath(CiTestCase):
60 multipath.mpath_partition_to_mpath_id(dev))77 multipath.mpath_partition_to_mpath_id(dev))
6178
62 def test_mpath_partition_to_mpath_id_none(self):79 def test_mpath_partition_to_mpath_id_none(self):
80 """mpath_part_to_mpath_id returns none if DM_MPATH missing."""
63 dev = self.random_string()81 dev = self.random_string()
64 self.m_udev.udevadm_info.return_value = {}82 self.m_udev.udevadm_info.return_value = {}
65 self.assertEqual(None,83 self.assertIsNone(multipath.mpath_partition_to_mpath_id(dev))
66 multipath.mpath_partition_to_mpath_id(dev))
6784
68 @mock.patch('curtin.block.multipath.os.path.exists')85 @mock.patch('curtin.block.multipath.os.path.exists')
69 @mock.patch('curtin.block.multipath.util.wait_for_removal')86 @mock.patch('curtin.block.multipath.util.wait_for_removal')
70 def test_remove_partition(self, m_wait, m_exists):87 def test_remove_partition(self, m_wait, m_exists):
88 """multipath.remove_partition runs dmsetup skips wait if dev gone."""
71 devpath = self.random_string()89 devpath = self.random_string()
72 m_exists.side_effect = iter([True, True, False])90 m_exists.side_effect = iter([True, True, False])
73 multipath.remove_partition(devpath)91 multipath.remove_partition(devpath)
74 expected = mock.call(['dmsetup', 'remove', devpath], rcs=[0, 1])92 expected = mock.call(
93 ['dmsetup', 'remove', '--force', '--retry', devpath])
75 self.m_subp.assert_has_calls([expected] * 3)94 self.m_subp.assert_has_calls([expected] * 3)
76 m_wait.assert_not_called()95 m_wait.assert_not_called()
77 self.assertEqual(3, self.m_udev.udevadm_settle.call_count)96 self.assertEqual(3, self.m_udev.udevadm_settle.call_count)
@@ -79,10 +98,12 @@ class TestMultipath(CiTestCase):
79 @mock.patch('curtin.block.multipath.os.path.exists')98 @mock.patch('curtin.block.multipath.os.path.exists')
80 @mock.patch('curtin.block.multipath.util.wait_for_removal')99 @mock.patch('curtin.block.multipath.util.wait_for_removal')
81 def test_remove_partition_waits(self, m_wait, m_exists):100 def test_remove_partition_waits(self, m_wait, m_exists):
101 """multipath.remove_partition runs dmsetup waits if dev still there."""
82 devpath = self.random_string()102 devpath = self.random_string()
83 m_exists.side_effect = iter([True, True, True])103 m_exists.side_effect = iter([True, True, True])
84 multipath.remove_partition(devpath, retries=3)104 multipath.remove_partition(devpath, retries=3)
85 expected = mock.call(['dmsetup', 'remove', devpath], rcs=[0, 1])105 expected = mock.call(
106 ['dmsetup', 'remove', '--force', '--retry', devpath])
86 self.m_subp.assert_has_calls([expected] * 3)107 self.m_subp.assert_has_calls([expected] * 3)
87 self.assertEqual(3, self.m_udev.udevadm_settle.call_count)108 self.assertEqual(3, self.m_udev.udevadm_settle.call_count)
88 self.assertEqual(1, m_wait.call_count)109 self.assertEqual(1, m_wait.call_count)
@@ -90,11 +111,13 @@ class TestMultipath(CiTestCase):
90 @mock.patch('curtin.block.multipath.os.path.exists')111 @mock.patch('curtin.block.multipath.os.path.exists')
91 @mock.patch('curtin.block.multipath.util.wait_for_removal')112 @mock.patch('curtin.block.multipath.util.wait_for_removal')
92 def test_remove_map(self, m_wait, m_exists):113 def test_remove_map(self, m_wait, m_exists):
114 """multipath.remove_map runs multipath -f skips wait if map gone."""
93 map_id = self.random_string()115 map_id = self.random_string()
94 devpath = '/dev/mapper/%s' % map_id116 devpath = '/dev/mapper/%s' % map_id
95 m_exists.side_effect = iter([True, True, False])117 m_exists.side_effect = iter([True, True, False])
96 multipath.remove_map(devpath)118 multipath.remove_map(devpath)
97 expected = mock.call(['multipath', '-f', devpath], rcs=[0, 1])119 expected = mock.call(
120 ['multipath', '-v3', '-R3', '-f', devpath], rcs=[0, 1])
98 self.m_subp.assert_has_calls([expected] * 3)121 self.m_subp.assert_has_calls([expected] * 3)
99 m_wait.assert_not_called()122 m_wait.assert_not_called()
100 self.assertEqual(3, self.m_udev.udevadm_settle.call_count)123 self.assertEqual(3, self.m_udev.udevadm_settle.call_count)
@@ -102,16 +125,19 @@ class TestMultipath(CiTestCase):
102 @mock.patch('curtin.block.multipath.os.path.exists')125 @mock.patch('curtin.block.multipath.os.path.exists')
103 @mock.patch('curtin.block.multipath.util.wait_for_removal')126 @mock.patch('curtin.block.multipath.util.wait_for_removal')
104 def test_remove_map_wait(self, m_wait, m_exists):127 def test_remove_map_wait(self, m_wait, m_exists):
128 """multipath.remove_map runs multipath -f wait if map remains."""
105 map_id = self.random_string()129 map_id = self.random_string()
106 devpath = '/dev/mapper/%s' % map_id130 devpath = '/dev/mapper/%s' % map_id
107 m_exists.side_effect = iter([True, True, True])131 m_exists.side_effect = iter([True, True, True])
108 multipath.remove_map(devpath, retries=3)132 multipath.remove_map(devpath, retries=3)
109 expected = mock.call(['multipath', '-f', devpath], rcs=[0, 1])133 expected = mock.call(
134 ['multipath', '-v3', '-R3', '-f', devpath], rcs=[0, 1])
110 self.m_subp.assert_has_calls([expected] * 3)135 self.m_subp.assert_has_calls([expected] * 3)
111 self.assertEqual(3, self.m_udev.udevadm_settle.call_count)136 self.assertEqual(3, self.m_udev.udevadm_settle.call_count)
112 self.assertEqual(1, m_wait.call_count)137 self.assertEqual(1, m_wait.call_count)
113138
114 def test_find_mpath_members(self):139 def test_find_mpath_members(self):
140 """find_mpath_members enumerates kernel block devs of a mpath_id."""
115 mp_id = 'mpatha'141 mp_id = 'mpatha'
116 paths = ['device=bar multipath=mpatha',142 paths = ['device=bar multipath=mpatha',
117 'device=wark multipath=mpatha']143 'device=wark multipath=mpatha']
@@ -120,6 +146,7 @@ class TestMultipath(CiTestCase):
120 sorted(multipath.find_mpath_members(mp_id)))146 sorted(multipath.find_mpath_members(mp_id)))
121147
122 def test_find_mpath_members_empty(self):148 def test_find_mpath_members_empty(self):
149 """find_mpath_members returns empty list if mpath_id not found."""
123 mp_id = self.random_string()150 mp_id = self.random_string()
124 paths = ['device=bar multipath=mpatha',151 paths = ['device=bar multipath=mpatha',
125 'device=wark multipath=mpatha']152 'device=wark multipath=mpatha']
@@ -127,6 +154,7 @@ class TestMultipath(CiTestCase):
127 self.assertEqual([], multipath.find_mpath_members(mp_id))154 self.assertEqual([], multipath.find_mpath_members(mp_id))
128155
129 def test_find_mpath_id(self):156 def test_find_mpath_id(self):
157 """find_mpath_id returns mpath_id if device is part of mpath group."""
130 mp_id = 'mpatha'158 mp_id = 'mpatha'
131 maps = ['sysfs=bar multipath=mpatha',159 maps = ['sysfs=bar multipath=mpatha',
132 'sysfs=wark multipath=mpatha']160 'sysfs=wark multipath=mpatha']
@@ -134,16 +162,69 @@ class TestMultipath(CiTestCase):
134 self.assertEqual(mp_id, multipath.find_mpath_id('/dev/bar'))162 self.assertEqual(mp_id, multipath.find_mpath_id('/dev/bar'))
135163
136 def test_find_mpath_id_name(self):164 def test_find_mpath_id_name(self):
165 """find_mpath_id_name returns the name if present in maps on match."""
137 maps = ['sysfs=bar multipath=mpatha name=friendly',166 maps = ['sysfs=bar multipath=mpatha name=friendly',
138 'sysfs=wark multipath=mpatha']167 'sysfs=wark multipath=mpatha']
139 self.m_subp.return_value = ("\n".join(maps), "")168 self.m_subp.return_value = ("\n".join(maps), "")
140 self.assertEqual('friendly', multipath.find_mpath_id('/dev/bar'))169 self.assertEqual('friendly', multipath.find_mpath_id('/dev/bar'))
141170
142 def test_find_mpath_id_none(self):171 def test_find_mpath_id_none(self):
172 """find_mpath_id_name returns none when device is not part of maps."""
143 maps = ['sysfs=bar multipath=mpatha',173 maps = ['sysfs=bar multipath=mpatha',
144 'sysfs=wark multipath=mpatha']174 'sysfs=wark multipath=mpatha']
145 self.m_subp.return_value = ("\n".join(maps), "")175 self.m_subp.return_value = ("\n".join(maps), "")
146 self.assertEqual(None, multipath.find_mpath_id('/dev/foo'))176 self.assertEqual(None, multipath.find_mpath_id('/dev/foo'))
147177
178 def test_dmname_to_blkdev_mapping(self):
179 """dmname_to_blkdev_mapping returns dmname to blkdevice dictionary."""
180 self.m_subp.return_value = (DMSETUP_LS_BLKDEV_OUTPUT, "")
181 expected_mapping = {
182 'mpatha': '/dev/dm-0',
183 'mpatha-part1': '/dev/dm-1',
184 '1gb zero': '/dev/dm-2',
185 }
186 self.assertEqual(expected_mapping,
187 multipath.dmname_to_blkdev_mapping())
188
189 def test_dmname_to_blkdev_mapping_empty(self):
190 """dmname_to_blkdev_mapping returns empty dict when dmsetup is empty.
191 """
192 self.m_subp.return_value = ("No devices found", "")
193 expected_mapping = {}
194 self.assertEqual(expected_mapping,
195 multipath.dmname_to_blkdev_mapping())
196
197 @mock.patch('curtin.block.multipath.dmname_to_blkdev_mapping')
198 def test_find_mpath_id_by_parent(self, m_dmmap):
199 """find_mpath_id_by_parent returns device mapper blk for given DM_NAME.
200 """
201 m_dmmap.return_value = {
202 'mpatha': '/dev/dm-0', 'mpatha-part1': '/dev/dm-1'}
203 mpath_id = 'mpatha'
204 expected_result = ('mpatha-part1', '/dev/dm-1')
205 self.assertEqual(
206 expected_result,
207 multipath.find_mpath_id_by_parent(mpath_id, partnum=1))
208
209 def test_find_mpath_id_by_path(self):
210 """find_mpath_id_by_path returns the mp_id if specified device is
211 member.
212 """
213 mp_id = 'mpatha'
214 paths = ['device=bar multipath=mpatha',
215 'device=wark multipath=mpathb']
216 self.m_subp.return_value = ("\n".join(paths), "")
217 self.assertEqual(mp_id, multipath.find_mpath_id_by_path('/dev/bar'))
218
219 def test_find_mpath_id_by_path_returns_none_not_found(self):
220 """find_mpath_id_by_path returns None if specified device is not a
221 member.
222 """
223 mp_id = 'mpatha'
224 paths = ['device=bar multipath=%s' % mp_id,
225 'device=wark multipath=%s' % mp_id]
226 self.m_subp.return_value = ("\n".join(paths), "")
227 self.assertIsNone(multipath.find_mpath_id_by_path('/dev/xxx'))
228
148229
149# vi: ts=4 expandtab syntax=python230# vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_clear_holders.py b/tests/unittests/test_clear_holders.py
index b454ae4..0a8f2da 100644
--- a/tests/unittests/test_clear_holders.py
+++ b/tests/unittests/test_clear_holders.py
@@ -305,17 +305,22 @@ class TestClearHolders(CiTestCase):
305 self.assertTrue(mock_log.debug.called)305 self.assertTrue(mock_log.debug.called)
306 self.assertTrue(mock_log.critical.called)306 self.assertTrue(mock_log.critical.called)
307307
308 @mock.patch('curtin.block.multipath.find_mpath_id_by_path')
308 @mock.patch('curtin.block.clear_holders.is_swap_device')309 @mock.patch('curtin.block.clear_holders.is_swap_device')
309 @mock.patch('curtin.block.clear_holders.os.path.exists')310 @mock.patch('curtin.block.clear_holders.os.path.exists')
310 @mock.patch('curtin.block.clear_holders.LOG')311 @mock.patch('curtin.block.clear_holders.LOG')
311 @mock.patch('curtin.block.clear_holders.block')312 @mock.patch('curtin.block.clear_holders.block')
312 def test_clear_holders_wipe_superblock(self, mock_block, mock_log,313 def test_clear_holders_wipe_superblock(self, mock_block, mock_log,
313 mock_os_path, mock_swap):314 mock_os_path, mock_swap,
315 mock_mp_bp):
314 """test clear_holders.wipe_superblock handles errors right"""316 """test clear_holders.wipe_superblock handles errors right"""
315 mock_swap.return_value = False317 mock_swap.return_value = False
316 mock_os_path.return_value = False318 mock_os_path.return_value = False
317 mock_block.sysfs_to_devpath.return_value = self.test_blockdev319 mock_block.sysfs_to_devpath.return_value = self.test_blockdev
318 mock_block.is_extended_partition.return_value = True320 mock_block.is_extended_partition.return_value = True
321 mock_block.get_blockdev_for_partition.return_value = (
322 self.test_blockdev, None)
323 mock_mp_bp.return_value = None
319 clear_holders.wipe_superblock(self.test_syspath)324 clear_holders.wipe_superblock(self.test_syspath)
320 self.assertFalse(mock_block.wipe_volume.called)325 self.assertFalse(mock_block.wipe_volume.called)
321 mock_block.is_extended_partition.return_value = False326 mock_block.is_extended_partition.return_value = False
@@ -325,16 +330,21 @@ class TestClearHolders(CiTestCase):
325 mock_block.wipe_volume.assert_called_with(330 mock_block.wipe_volume.assert_called_with(
326 self.test_blockdev, exclusive=True, mode='superblock', strict=True)331 self.test_blockdev, exclusive=True, mode='superblock', strict=True)
327332
333 @mock.patch('curtin.block.multipath.find_mpath_id_by_path')
328 @mock.patch('curtin.block.clear_holders.is_swap_device')334 @mock.patch('curtin.block.clear_holders.is_swap_device')
329 @mock.patch('curtin.block.clear_holders.zfs')335 @mock.patch('curtin.block.clear_holders.zfs')
330 @mock.patch('curtin.block.clear_holders.LOG')336 @mock.patch('curtin.block.clear_holders.LOG')
331 @mock.patch('curtin.block.clear_holders.block')337 @mock.patch('curtin.block.clear_holders.block')
332 def test_clear_holders_wipe_superblock_zfs(self, mock_block, mock_log,338 def test_clear_holders_wipe_superblock_zfs(self, mock_block, mock_log,
333 mock_zfs, mock_swap):339 mock_zfs, mock_swap,
340 mock_mp_bp):
334 """test clear_holders.wipe_superblock handles zfs member"""341 """test clear_holders.wipe_superblock handles zfs member"""
335 mock_swap.return_value = False342 mock_swap.return_value = False
336 mock_block.sysfs_to_devpath.return_value = self.test_blockdev343 mock_block.sysfs_to_devpath.return_value = self.test_blockdev
337 mock_block.is_extended_partition.return_value = True344 mock_block.is_extended_partition.return_value = True
345 mock_block.get_blockdev_for_partition.return_value = (
346 self.test_blockdev, None)
347 mock_mp_bp.return_value = None
338 clear_holders.wipe_superblock(self.test_syspath)348 clear_holders.wipe_superblock(self.test_syspath)
339 self.assertFalse(mock_block.wipe_volume.called)349 self.assertFalse(mock_block.wipe_volume.called)
340 mock_block.is_extended_partition.return_value = False350 mock_block.is_extended_partition.return_value = False
@@ -348,16 +358,21 @@ class TestClearHolders(CiTestCase):
348 mock_block.wipe_volume.assert_called_with(358 mock_block.wipe_volume.assert_called_with(
349 self.test_blockdev, exclusive=True, mode='superblock', strict=True)359 self.test_blockdev, exclusive=True, mode='superblock', strict=True)
350360
361 @mock.patch('curtin.block.multipath.find_mpath_id_by_path')
351 @mock.patch('curtin.block.clear_holders.is_swap_device')362 @mock.patch('curtin.block.clear_holders.is_swap_device')
352 @mock.patch('curtin.block.clear_holders.zfs')363 @mock.patch('curtin.block.clear_holders.zfs')
353 @mock.patch('curtin.block.clear_holders.LOG')364 @mock.patch('curtin.block.clear_holders.LOG')
354 @mock.patch('curtin.block.clear_holders.block')365 @mock.patch('curtin.block.clear_holders.block')
355 def test_clear_holders_wipe_superblock_no_zfs(self, mock_block, mock_log,366 def test_clear_holders_wipe_superblock_no_zfs(self, mock_block, mock_log,
356 mock_zfs, mock_swap):367 mock_zfs, mock_swap,
368 mock_mp_bp):
357 """test clear_holders.wipe_superblock checks zfs supported"""369 """test clear_holders.wipe_superblock checks zfs supported"""
358 mock_swap.return_value = False370 mock_swap.return_value = False
359 mock_block.sysfs_to_devpath.return_value = self.test_blockdev371 mock_block.sysfs_to_devpath.return_value = self.test_blockdev
360 mock_block.is_extended_partition.return_value = True372 mock_block.is_extended_partition.return_value = True
373 mock_block.get_blockdev_for_partition.return_value = (
374 self.test_blockdev, None)
375 mock_mp_bp.return_value = None
361 clear_holders.wipe_superblock(self.test_syspath)376 clear_holders.wipe_superblock(self.test_syspath)
362 self.assertFalse(mock_block.wipe_volume.called)377 self.assertFalse(mock_block.wipe_volume.called)
363 mock_block.is_extended_partition.return_value = False378 mock_block.is_extended_partition.return_value = False
@@ -372,13 +387,14 @@ class TestClearHolders(CiTestCase):
372 mock_block.wipe_volume.assert_called_with(387 mock_block.wipe_volume.assert_called_with(
373 self.test_blockdev, exclusive=True, mode='superblock', strict=True)388 self.test_blockdev, exclusive=True, mode='superblock', strict=True)
374389
390 @mock.patch('curtin.block.multipath.find_mpath_id_by_path')
375 @mock.patch('curtin.block.clear_holders.is_swap_device')391 @mock.patch('curtin.block.clear_holders.is_swap_device')
376 @mock.patch('curtin.block.clear_holders.zfs')392 @mock.patch('curtin.block.clear_holders.zfs')
377 @mock.patch('curtin.block.clear_holders.LOG')393 @mock.patch('curtin.block.clear_holders.LOG')
378 @mock.patch('curtin.block.clear_holders.block')394 @mock.patch('curtin.block.clear_holders.block')
379 def test_clear_holders_wipe_superblock_zfs_no_utils(self, mock_block,395 def test_clear_holders_wipe_superblock_zfs_no_utils(self, mock_block,
380 mock_log, mock_zfs,396 mock_log, mock_zfs,
381 mock_swap):397 mock_swap, mock_mp_bp):
382 """test clear_holders.wipe_superblock handles missing zpool cmd"""398 """test clear_holders.wipe_superblock handles missing zpool cmd"""
383 mock_swap.return_value = False399 mock_swap.return_value = False
384 mock_block.sysfs_to_devpath.return_value = self.test_blockdev400 mock_block.sysfs_to_devpath.return_value = self.test_blockdev
@@ -386,6 +402,9 @@ class TestClearHolders(CiTestCase):
386 clear_holders.wipe_superblock(self.test_syspath)402 clear_holders.wipe_superblock(self.test_syspath)
387 self.assertFalse(mock_block.wipe_volume.called)403 self.assertFalse(mock_block.wipe_volume.called)
388 mock_block.is_extended_partition.return_value = False404 mock_block.is_extended_partition.return_value = False
405 mock_block.get_blockdev_for_partition.return_value = (
406 self.test_blockdev, None)
407 mock_mp_bp.return_value = None
389 mock_block.is_zfs_member.return_value = True408 mock_block.is_zfs_member.return_value = True
390 mock_zfs.zfs_supported.return_value = True409 mock_zfs.zfs_supported.return_value = True
391 mock_zfs.device_to_poolname.return_value = 'fake_pool'410 mock_zfs.device_to_poolname.return_value = 'fake_pool'
@@ -400,17 +419,21 @@ class TestClearHolders(CiTestCase):
400 mock_block.wipe_volume.assert_called_with(419 mock_block.wipe_volume.assert_called_with(
401 self.test_blockdev, exclusive=True, mode='superblock', strict=True)420 self.test_blockdev, exclusive=True, mode='superblock', strict=True)
402421
422 @mock.patch('curtin.block.multipath.find_mpath_id_by_path')
403 @mock.patch('curtin.block.clear_holders.is_swap_device')423 @mock.patch('curtin.block.clear_holders.is_swap_device')
404 @mock.patch('curtin.block.clear_holders.time')424 @mock.patch('curtin.block.clear_holders.time')
405 @mock.patch('curtin.block.clear_holders.LOG')425 @mock.patch('curtin.block.clear_holders.LOG')
406 @mock.patch('curtin.block.clear_holders.block')426 @mock.patch('curtin.block.clear_holders.block')
407 def test_clear_holders_wipe_superblock_rereads_pt(self, mock_block,427 def test_clear_holders_wipe_superblock_rereads_pt(self, mock_block,
408 mock_log, m_time,428 mock_log, m_time,
409 mock_swap):429 mock_swap, mock_mp_bp):
410 """test clear_holders.wipe_superblock re-reads partition table"""430 """test clear_holders.wipe_superblock re-reads partition table"""
411 mock_swap.return_value = False431 mock_swap.return_value = False
412 mock_block.sysfs_to_devpath.return_value = self.test_blockdev432 mock_block.sysfs_to_devpath.return_value = self.test_blockdev
413 mock_block.is_extended_partition.return_value = False433 mock_block.is_extended_partition.return_value = False
434 mock_block.get_blockdev_for_partition.return_value = (
435 self.test_blockdev, None)
436 mock_mp_bp.return_value = None
414 mock_block.is_zfs_member.return_value = False437 mock_block.is_zfs_member.return_value = False
415 mock_block.get_sysfs_partitions.side_effect = iter([438 mock_block.get_sysfs_partitions.side_effect = iter([
416 ['p1', 'p2'], # has partitions before wipe439 ['p1', 'p2'], # has partitions before wipe
@@ -426,17 +449,22 @@ class TestClearHolders(CiTestCase):
426 mock_block.rescan_block_devices.assert_has_calls(449 mock_block.rescan_block_devices.assert_has_calls(
427 [mock.call(devices=[self.test_blockdev])] * 2)450 [mock.call(devices=[self.test_blockdev])] * 2)
428451
452 @mock.patch('curtin.block.multipath.find_mpath_id_by_path')
429 @mock.patch('curtin.block.clear_holders.is_swap_device')453 @mock.patch('curtin.block.clear_holders.is_swap_device')
430 @mock.patch('curtin.block.clear_holders.time')454 @mock.patch('curtin.block.clear_holders.time')
431 @mock.patch('curtin.block.clear_holders.LOG')455 @mock.patch('curtin.block.clear_holders.LOG')
432 @mock.patch('curtin.block.clear_holders.block')456 @mock.patch('curtin.block.clear_holders.block')
433 def test_clear_holders_wipe_superblock_rereads_pt_oserr(self, mock_block,457 def test_clear_holders_wipe_superblock_rereads_pt_oserr(self, mock_block,
434 mock_log, m_time,458 mock_log, m_time,
435 mock_swap):459 mock_swap,
460 mock_mp_bp):
436 """test clear_holders.wipe_superblock re-reads ptable handles oserr"""461 """test clear_holders.wipe_superblock re-reads ptable handles oserr"""
437 mock_swap.return_value = False462 mock_swap.return_value = False
438 mock_block.sysfs_to_devpath.return_value = self.test_blockdev463 mock_block.sysfs_to_devpath.return_value = self.test_blockdev
439 mock_block.is_extended_partition.return_value = False464 mock_block.is_extended_partition.return_value = False
465 mock_block.get_blockdev_for_partition.return_value = (
466 self.test_blockdev, None)
467 mock_mp_bp.return_value = None
440 mock_block.is_zfs_member.return_value = False468 mock_block.is_zfs_member.return_value = False
441 mock_block.get_sysfs_partitions.side_effect = iter([469 mock_block.get_sysfs_partitions.side_effect = iter([
442 ['p1', 'p2'], # has partitions before wipe470 ['p1', 'p2'], # has partitions before wipe
diff --git a/tests/vmtests/test_multipath.py b/tests/vmtests/test_multipath.py
index 339441e..c807a9b 100644
--- a/tests/vmtests/test_multipath.py
+++ b/tests/vmtests/test_multipath.py
@@ -11,6 +11,7 @@ import textwrap
1111
12class TestMultipathBasicAbs(VMBaseClass):12class TestMultipathBasicAbs(VMBaseClass):
13 conf_file = "examples/tests/multipath.yaml"13 conf_file = "examples/tests/multipath.yaml"
14 dirty_disks = True
14 test_type = 'storage'15 test_type = 'storage'
15 multipath = True16 multipath = True
16 disk_driver = 'scsi-hd'17 disk_driver = 'scsi-hd'

Subscribers

People subscribed via source and target branches