Merge ~mwhudson/curtin:fix-multipath-partition-verification-2 into curtin:master

Proposed by Michael Hudson-Doyle
Status: Merged
Approved by: Michael Hudson-Doyle
Approved revision: 283184ef83e46c5643da4b58c349343aabef1737
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~mwhudson/curtin:fix-multipath-partition-verification-2
Merge into: curtin:master
Diff against target: 183 lines (+33/-41)
4 files modified
curtin/block/__init__.py (+8/-4)
curtin/block/multipath.py (+4/-4)
tests/unittests/test_block.py (+9/-27)
tests/unittests/test_block_multipath.py (+12/-6)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Ryan Harper (community) Approve
Review via email: mp+396462@code.launchpad.net

Commit message

block: fixes for verifying existing multipath partitions

Verifying a partition on a multipath disk currently fails for two
reasons:

 1. get_blockdev_for_partition does not know how to go from a multipath
    partition to a multipath disk, so sfdisk_info ends up calling sfdisk
    on the partition, which fails.
 2. sfdisk --json /dev/dm-X prints /dev/mapper paths for the partition
    paths but compares them against the fully deferenced /dev/dm-Y path
    for the partition.

2 is easily fixed by resolving symlinks before comparing nodes. 1
can be fixed with a little udev poking but then sys_block_path
returns paths like /sys/class/block/dm-1/dm-2 for a partition of
a multipath disk, which doesn't exist. Luckily, nodes for
partitions exist directly in /sys/class/block and have done since
/sys/class/block was added in 2008 (see kernel commit
edfaa7c36574f1bf09c65ad602412db9da5f96bf) so we can just remove
the call to get_blockdev_for_partition from sys_block_read.

Description of the change

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

Thanks for the review.

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

> Michael Hudson-Doyle (mwhudson) wrote 6 minutes ago:
> I'm not sure what "really disks" means on a philosophical level <wink> but yes

Maybe I'm missing the <wink> here, but just for the folks following along I'll
be explicit.

I'm referring to multipath partitions which a linear mapping to offsets on the
multipath path disk in the device-mapper table. Device-mapper will create
additional device-mapper devices (/dev/dm-1, /dev/dm-2) which according to
sysfs, udev and any other linux tool, are of type disk. For example, in sysfs
/sys/class/block/dm-1 can be a "partition" and /sys/class/block/dm-1/partition
will not exist.

The mutipath software stack has conventions to determine which dm devices are
representing partitions by exporting values like DM_PART and prefixing dm
names with part- ; etc.

This code makes use of these multipath convensions.

> this is for multipath. I found that there was a currently-unused function in
> multipath.py that does most of what we want so I changed it to return the
> partition number as well and changed this code. (it used udev rather than
> sysfs pokery but well. no real difference).

OK, sounds good! I'll review.

> On the subject of mocks, get_blockdev_for_partition is currently untested.
> Tests could be written but you'd have to mock so many filesystem accesses that
> it would be incredibly fragile and I'm not sure what value it would bring.
>
> You must have thought about a more generic way to mock the filesystem by now,
> surely, like something where you supply a dictionary mapping paths to
> content and mocks for the various os.path.* functions get set up for you?
> Did you ever write anything for this I can steal? :)

I think the closes thing we have is using tests/unittests/helpers.py:dir2dict
and populate_dir and FilesystemMocking unittest class'[1] reRoot() method which
effectively applies a chroot prefix to the operations.

1. https://github.com/canonical/cloud-init/blob/master/cloudinit/tests/helpers.py#L242

Revision history for this message
Ryan Harper (raharper) :
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

> > Michael Hudson-Doyle (mwhudson) wrote 6 minutes ago:
> > I'm not sure what "really disks" means on a philosophical level <wink> but
> yes
>
> Maybe I'm missing the <wink> here, but just for the folks following along I'll
> be explicit.

Well mostly I'm just being a bit silly.

> I'm referring to multipath partitions which a linear mapping to offsets on the
> multipath path disk in the device-mapper table. Device-mapper will create
> additional device-mapper devices (/dev/dm-1, /dev/dm-2) which according to
> sysfs, udev and any other linux tool, are of type disk. For example, in sysfs
> /sys/class/block/dm-1 can be a "partition" and /sys/class/block/dm-1/partition
> will not exist.

Yeah, although I suspect their classification as "disk" is more by default vs them having any better classification.

> The mutipath software stack has conventions to determine which dm devices are
> representing partitions by exporting values like DM_PART and prefixing dm
> names with part- ; etc.

Yeah, I think it's worth saying that these conventions are pretty baked-in: none of it is enforced (afaict) by the kernel or anything, but if you try to set up a multipath device with a device mapper "uuid" that does not start with "mpath-", you're going to have a bad time.

It's a bit frustrating in a way that this information gets spread around so much -- most of this stuff is encoded by multipathd into the dm "uuid" and then read out of the uuid again by 11-dm-mpath.rules / kpartx_id. It's a bit hard to know which information source to treat as canonical. Probably reading things from udev is the right level of abstraction.

> This code makes use of these multipath convensions.
>
>
> > this is for multipath. I found that there was a currently-unused function in
> > multipath.py that does most of what we want so I changed it to return the
> > partition number as well and changed this code. (it used udev rather than
> > sysfs pokery but well. no real difference).
>
> OK, sounds good! I'll review.
>
> > On the subject of mocks, get_blockdev_for_partition is currently untested.
> > Tests could be written but you'd have to mock so many filesystem accesses
> that
> > it would be incredibly fragile and I'm not sure what value it would bring.
> >
> > You must have thought about a more generic way to mock the filesystem by
> now,
> > surely, like something where you supply a dictionary mapping paths to
> > content and mocks for the various os.path.* functions get set up for you?
> > Did you ever write anything for this I can steal? :)
>
>
> I think the closes thing we have is using tests/unittests/helpers.py:dir2dict
> and populate_dir and FilesystemMocking unittest class'[1] reRoot() method
> which
> effectively applies a chroot prefix to the operations.

Yeah, that's the sort of thing I was thinking of, although I guess I was more imagining something in memory (which would be more effort to implement but presumably faster in execution).

> 1. https://github.com/canonical/cloud-
> init/blob/master/cloudinit/tests/helpers.py#L242

Revision history for this message
Michael Hudson-Doyle (mwhudson) :
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)
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

FWIW I tested this and it allowed me to reuse multipath partitions.

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 :

Looks good.

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

Commit message lints:
- Line #17 has 44 too many characters. Line starts with: "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=edfaa7c36574f1bf09c65ad602412db9da5f96b"...

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/curtin/block/__init__.py b/curtin/block/__init__.py
2index 0cf0866..1b33002 100644
3--- a/curtin/block/__init__.py
4+++ b/curtin/block/__init__.py
5@@ -158,9 +158,6 @@ def sys_block_path(devname, add=None, strict=True):
6 devname = os.path.normpath(devname)
7 if devname.startswith('/dev/') and not os.path.exists(devname):
8 LOG.warning('block.sys_block_path: devname %s does not exist', devname)
9- (parent, partnum) = get_blockdev_for_partition(devname, strict=strict)
10- if partnum:
11- toks.append(path_to_kname(parent))
12
13 toks.append(path_to_kname(devname))
14
15@@ -309,7 +306,7 @@ def get_partition_sfdisk_info(devpath, sfdisk_info=None):
16 sfdisk_info = sfdisk_info(devpath)
17
18 entry = [part for part in sfdisk_info['partitions']
19- if part['node'] == devpath]
20+ if os.path.realpath(part['node']) == os.path.realpath(devpath)]
21 if len(entry) != 1:
22 raise RuntimeError('Device %s not present in sfdisk dump:\n%s' %
23 devpath, util.json_dumps(sfdisk_info))
24@@ -421,6 +418,13 @@ def get_blockdev_for_partition(devpath, strict=True):
25 if strict and not os.path.exists(syspath):
26 raise OSError("%s had no syspath (%s)" % (devpath, syspath))
27
28+ if rpath.startswith('/dev/dm-'):
29+ parent_info = multipath.mpath_partition_to_mpath_id_and_partnumber(
30+ rpath)
31+ if parent_info is not None:
32+ mpath_id, ptnum = parent_info
33+ return os.path.realpath('/dev/mapper/' + mpath_id), ptnum
34+
35 ptpath = os.path.join(syspath, "partition")
36 if not os.path.exists(ptpath):
37 return (rpath, None)
38diff --git a/curtin/block/multipath.py b/curtin/block/multipath.py
39index 7ad1791..a28e930 100644
40--- a/curtin/block/multipath.py
41+++ b/curtin/block/multipath.py
42@@ -88,11 +88,11 @@ def is_mpath_partition(devpath, info=None):
43 return result
44
45
46-def mpath_partition_to_mpath_id(devpath):
47- """ Return the mpath id of a multipath partition. """
48+def mpath_partition_to_mpath_id_and_partnumber(devpath):
49+ """ Return the mpath id and partition number of a multipath partition. """
50 info = udev.udevadm_info(devpath)
51- if 'DM_MPATH' in info:
52- return info['DM_MPATH']
53+ if 'DM_MPATH' in info and 'DM_PART' in info:
54+ return info['DM_MPATH'], info['DM_PART']
55
56 return None
57
58diff --git a/tests/unittests/test_block.py b/tests/unittests/test_block.py
59index 78e331d..d96a4a8 100644
60--- a/tests/unittests/test_block.py
61+++ b/tests/unittests/test_block.py
62@@ -193,47 +193,37 @@ class TestBlock(CiTestCase):
63
64
65 class TestSysBlockPath(CiTestCase):
66- @mock.patch("curtin.block.get_blockdev_for_partition")
67 @mock.patch("os.path.exists")
68- def test_existing_valid_devname(self, m_os_path_exists, m_get_blk):
69+ def test_existing_valid_devname(self, m_os_path_exists):
70 m_os_path_exists.return_value = True
71- m_get_blk.return_value = ('foodevice', None)
72 self.assertEqual('/sys/class/block/foodevice',
73 block.sys_block_path("foodevice"))
74
75- @mock.patch("curtin.block.get_blockdev_for_partition")
76 @mock.patch("os.path.exists")
77- def test_existing_devpath_allowed(self, m_os_path_exists, m_get_blk):
78+ def test_existing_devpath_allowed(self, m_os_path_exists):
79 m_os_path_exists.return_value = True
80- m_get_blk.return_value = ('foodev', None)
81 self.assertEqual('/sys/class/block/foodev',
82 block.sys_block_path("/dev/foodev"))
83
84- @mock.patch("curtin.block.get_blockdev_for_partition")
85 @mock.patch("os.path.exists")
86- def test_add_works(self, m_os_path_exists, m_get_blk):
87+ def test_add_works(self, m_os_path_exists):
88 m_os_path_exists.return_value = True
89- m_get_blk.return_value = ('foodev', None)
90 self.assertEqual('/sys/class/block/foodev/md/b',
91 block.sys_block_path("/dev/foodev", "md/b"))
92
93- @mock.patch("curtin.block.get_blockdev_for_partition")
94 @mock.patch("os.path.exists")
95- def test_add_works_leading_slash(self, m_os_path_exists, m_get_blk):
96+ def test_add_works_leading_slash(self, m_os_path_exists):
97 m_os_path_exists.return_value = True
98- m_get_blk.return_value = ('foodev', None)
99 self.assertEqual('/sys/class/block/foodev/md/b',
100 block.sys_block_path("/dev/foodev", "/md/b"))
101
102- @mock.patch("curtin.block.get_blockdev_for_partition")
103 @mock.patch("os.path.exists")
104- def test_invalid_devname_raises(self, m_os_path_exists, m_get_blk):
105+ def test_invalid_devname_raises(self, m_os_path_exists):
106 m_os_path_exists.return_value = False
107- with self.assertRaises(ValueError):
108+ with self.assertRaises(OSError):
109 block.sys_block_path("foodevice")
110
111- @mock.patch("curtin.block.get_blockdev_for_partition")
112- def test_invalid_with_add(self, m_get_blk):
113+ def test_invalid_with_add(self):
114 # test the device exists, but 'add' does not
115 # path_exists returns true unless 'md/device' is in it
116 # so /sys/class/foodev/ exists, but not /sys/class/foodev/md/device
117@@ -242,28 +232,20 @@ class TestSysBlockPath(CiTestCase):
118 def path_exists(path):
119 return add not in path
120
121- m_get_blk.return_value = ("foodev", None)
122 with mock.patch('os.path.exists', side_effect=path_exists):
123 self.assertRaises(OSError, block.sys_block_path, "foodev", add)
124
125- @mock.patch("curtin.block.get_blockdev_for_partition")
126 @mock.patch("os.path.exists")
127- def test_not_strict_does_not_care(self, m_os_path_exists, m_get_blk):
128+ def test_not_strict_does_not_care(self, m_os_path_exists):
129 m_os_path_exists.return_value = False
130- m_get_blk.return_value = ('foodev', None)
131 self.assertEqual('/sys/class/block/foodev/md/b',
132 block.sys_block_path("foodev", "/md/b", strict=False))
133
134- @mock.patch('curtin.block.get_blockdev_for_partition')
135 @mock.patch('os.path.exists')
136- def test_cciss_sysfs_path(self, m_os_path_exists, m_get_blk):
137+ def test_cciss_sysfs_path(self, m_os_path_exists):
138 m_os_path_exists.return_value = True
139- m_get_blk.return_value = ('cciss!c0d0', None)
140 self.assertEqual('/sys/class/block/cciss!c0d0',
141 block.sys_block_path('/dev/cciss/c0d0'))
142- m_get_blk.return_value = ('cciss!c0d0', 1)
143- self.assertEqual('/sys/class/block/cciss!c0d0/cciss!c0d0p1',
144- block.sys_block_path('/dev/cciss/c0d0p1'))
145
146
147 class TestWipeFile(CiTestCase):
148diff --git a/tests/unittests/test_block_multipath.py b/tests/unittests/test_block_multipath.py
149index 96cbcba..c56ec1e 100644
150--- a/tests/unittests/test_block_multipath.py
151+++ b/tests/unittests/test_block_multipath.py
152@@ -82,19 +82,25 @@ class TestMultipath(CiTestCase):
153 """is_mpath_member returns false if DM_PART is not present for dev."""
154 self.assertFalse(multipath.is_mpath_partition(self.random_string()))
155
156- def test_mpath_partition_to_mpath_id(self):
157+ def test_mpath_partition_to_mpath_id_and_partnumber(self):
158 """mpath_part_to_mpath_id extracts MD_MPATH value from mp partition."""
159 dev = self.random_string()
160 mpath_id = self.random_string()
161- self.m_udev.udevadm_info.return_value = {'DM_MPATH': mpath_id}
162- self.assertEqual(mpath_id,
163- multipath.mpath_partition_to_mpath_id(dev))
164+ mpath_ptnum = self.random_string()
165+ self.m_udev.udevadm_info.return_value = {
166+ 'DM_MPATH': mpath_id,
167+ 'DM_PART': mpath_ptnum,
168+ }
169+ self.assertEqual(
170+ (mpath_id, mpath_ptnum),
171+ multipath.mpath_partition_to_mpath_id_and_partnumber(dev))
172
173- def test_mpath_partition_to_mpath_id_none(self):
174+ def test_mpath_partition_to_mpath_id_and_partnumber_none(self):
175 """mpath_part_to_mpath_id returns none if DM_MPATH missing."""
176 dev = self.random_string()
177 self.m_udev.udevadm_info.return_value = {}
178- self.assertIsNone(multipath.mpath_partition_to_mpath_id(dev))
179+ self.assertIsNone(
180+ multipath.mpath_partition_to_mpath_id_and_partnumber(dev))
181
182 @mock.patch('curtin.block.multipath.os.path.exists')
183 @mock.patch('curtin.block.multipath.util.wait_for_removal')

Subscribers

People subscribed via source and target branches