Merge ~mwhudson/curtin:lp-1893818 into curtin:master

Proposed by Michael Hudson-Doyle
Status: Merged
Approved by: Michael Hudson-Doyle
Approved revision: 9348f089bf171d3d1df781dc0217a1508ad7aa81
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~mwhudson/curtin:lp-1893818
Merge into: curtin:master
Diff against target: 641 lines (+204/-243)
6 files modified
curtin/block/multipath.py (+4/-5)
curtin/storage_config.py (+61/-92)
examples/tests/multipath-reuse.yaml (+61/-0)
tests/unittests/test_block_multipath.py (+27/-10)
tests/unittests/test_storage_config.py (+35/-136)
tests/vmtests/test_multipath.py (+16/-0)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Paride Legovini Needs Fixing
Review via email: mp+392353@code.launchpad.net

Commit message

storage_config: return one type: disk action per multipathed disk

Currently extract_storage_config returns one type: disk action
for every member of a multipathed disk and type: partition
actions for each partition of each disk. This works by generating
a type: disk action for each disk and ignoring the block device
data for the /dev/dm-X device for the multipathed disk.

But in groovy+, the udev rule from multipath-tools that has
always attempted to remove the devices nodes for the partitions
of a disk that is a multipath member actually succeeds, and
trying to generate a type: partition action for a partition with
no underlying device node makes things blow up.

Instead, this branch generates type: disk and type: partitions
actions from the /dev/dm-X nodes for the mutipathed disk and its
partitions, and ignores and disks and partitions that are members
of a multipathed disk.

LP: #1893818

To post a comment you must log in.
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

I haven't tested this on real hw yet!

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

Hi Michael,

Do you have access to the CI build logs above?

I can reproduce the failure locally by running `tox` on your branch.

review: Needs Fixing
Revision history for this message
Dimitri John Ledkov (xnox) wrote :

Does MAAS use this codepath? As far as I recall, MAAS has no concept of multipath and may get confused?!

~mwhudson/curtin:lp-1893818 updated
2268251... by Michael Hudson-Doyle

oops, had not noticed that I had made this test depend on jsonschema

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
~mwhudson/curtin:lp-1893818 updated
8f86d08... by Michael Hudson-Doyle

ffs

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

> Does MAAS use this codepath? As far as I recall, MAAS has no concept of
> multipath and may get confused?!

Pretty sure MAAS doesn't use this code at all.

Revision history for this message
Dimitri John Ledkov (xnox) wrote :

In general this is good, and implements things as have been originally asked for in the "multipath google doc of doom".

See minor comments.

Also please rebase commits to have only descriptive commit messages without fixups =)

Revision history for this message
Paride Legovini (paride) wrote :

@xnox: the autolander squashes the MPs using the proposed commit message, so the fixups do not really hurt unless you merge manually.

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

Thanks for the comments. I think I'm not going to make any changes in response to them right now.

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

> But in groovy+, the udev rule from multipath-tools that has
> always attempted to remove the devices nodes for the partitions
> of a disk that is a multipath member actually succeeds, and

Won't this break curtin on older multipaths that don't have this rule?

Or will this work with older multipath output since we're only referring to
the mpath dev (rather than a path of the mpath device)?

Some comments inline.

~mwhudson/curtin:lp-1893818 updated
06825b6... by Michael Hudson-Doyle

simplify is_mpath_partition, thanks ryan

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

> > But in groovy+, the udev rule from multipath-tools that has
> > always attempted to remove the devices nodes for the partitions
> > of a disk that is a multipath member actually succeeds, and
>
> Won't this break curtin on older multipaths that don't have this rule?

No, if the device node for a partition of a disk that is a multipath member is still present, this branch ignores it (or at least that's the intent...)

> Or will this work with older multipath output since we're only referring to
> the mpath dev (rather than a path of the mpath device)?

Er, I think so. Not completely sure about the distinction you're making here.

> Some comments inline.

Replies inline.

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 :

> > > But in groovy+, the udev rule from multipath-tools that has
> > > always attempted to remove the devices nodes for the partitions
> > > of a disk that is a multipath member actually succeeds, and
> >
> > Won't this break curtin on older multipaths that don't have this rule?
>
> No, if the device node for a partition of a disk that is a multipath member is still present, this branch ignores it (or at least that's the intent...)
>
> > Or will this work with older multipath output since we're only referring to
> > the mpath dev (rather than a path of the mpath device)?
>
> Er, I think so. Not completely sure about the distinction you're making here.

In the description, in groovy+ we will no longer have the partitions on the
path members IIUC. For example (say a 2 path disk with 1 partition) we'd see:

/dev/sda
/dev/sda1
/dev/sdb
/dev/sdb1
/dev/dm-0 (mpatha)
/dev/dm-1 (mpatha-part1)

And on Groovy+ we see:

/dev/sda
/dev/sdb
/dev/dm-0 (mpatha)
/dev/dm-1 (mpatha-part1)

Is that correct?

My concern was if the single-path partitions are present (as they are on
Focal and older) does the new code still handle things OK?

I *think* it's yes due to the fact that curtin knows how to create partitions
on MP devices since 20.04.

I've added some replies in-line, awkwardly, you have to select your previous
commit to see those.

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

> > > > But in groovy+, the udev rule from multipath-tools that has
> > > > always attempted to remove the devices nodes for the partitions
> > > > of a disk that is a multipath member actually succeeds, and
> > >
> > > Won't this break curtin on older multipaths that don't have this rule?
> >
> > No, if the device node for a partition of a disk that is a multipath member
> is still present, this branch ignores it (or at least that's the intent...)
> >
> > > Or will this work with older multipath output since we're only referring
> to
> > > the mpath dev (rather than a path of the mpath device)?
> >
> > Er, I think so. Not completely sure about the distinction you're making
> here.
>
> In the description, in groovy+ we will no longer have the partitions on the
> path members IIUC. For example (say a 2 path disk with 1 partition) we'd see:
>
> /dev/sda
> /dev/sda1
> /dev/sdb
> /dev/sdb1
> /dev/dm-0 (mpatha)
> /dev/dm-1 (mpatha-part1)
>
> And on Groovy+ we see:
>
> /dev/sda
> /dev/sdb
> /dev/dm-0 (mpatha)
> /dev/dm-1 (mpatha-part1)
>
> Is that correct?

Yes.

> My concern was if the single-path partitions are present (as they are on
> Focal and older) does the new code still handle things OK?

Oh I see, you're worried about the downstream impact of this: whether the storage configs produced by curtin after this change can actually be installed. That's a good question!

> I *think* it's yes due to the fact that curtin knows how to create partitions
> on MP devices since 20.04.

I think you're probably right but I should also do some testing of this situation.

> I've added some replies in-line, awkwardly, you have to select your previous
> commit to see those.

Replied.

Revision history for this message
Michael Hudson-Doyle (mwhudson) :
~mwhudson/curtin:lp-1893818 updated
d012706... by Michael Hudson-Doyle

make is_mpath_partition more correct

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
~mwhudson/curtin:lp-1893818 updated
f3734d2... by Michael Hudson-Doyle

a qemu test had DM_WWN=0xQEMU_QEMU_HARDDISK_001 somehow

6d8ae26... by Michael Hudson-Doyle

disable pretty path for multipath disks

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
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
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
~mwhudson/curtin:lp-1893818 updated
fee2947... by Michael Hudson-Doyle

remove commented out code

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)
~mwhudson/curtin:lp-1893818 updated
3f56616... by Michael Hudson-Doyle

another

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
~mwhudson/curtin:lp-1893818 updated
844a1c1... by Michael Hudson-Doyle

more tests, surely there is a more systematic way to do this

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
~mwhudson/curtin:lp-1893818 updated
fe9f59f... by Michael Hudson-Doyle

flake8

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
~mwhudson/curtin:lp-1893818 updated
710522f... by Michael Hudson-Doyle

lint

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
~mwhudson/curtin:lp-1893818 updated
cccce7b... by Michael Hudson-Doyle

light self review

98bacc6... by Michael Hudson-Doyle

don't need these changes any more

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
Ryan Harper (raharper) wrote :

Some more inline questions.

I definitely want to discuss the devtype mpath-partition, mpath-disk values. We cannot write out a curtin storage config with type:mpath-disk type:mpath-partition values; they aren't in the block schema.

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

Thanks for the comments, some replies to the inline questions.

Can you look at https://code.launchpad.net/~mwhudson/curtin/+git/curtin/+merge/396462 first?

I changed the ProbertParser.is_mpath_* methods to just forward to the multipath.is_mpath_* functions, which meant I had to change one of the latter to work via udev. We don't need any of this knowledge even more spread around that it was already...

~mwhudson/curtin:lp-1893818 updated
d50afda... by Michael Hudson-Doyle

address review comments

b2f7156... by Michael Hudson-Doyle

clean up finding parent partitiontable from partition

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)
~mwhudson/curtin:lp-1893818 updated
e8da9bb... by Michael Hudson-Doyle

flakes

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

So I think this is ready to go. I've tested it in KVM to install to both new and existing partitions on a multipath disk and it worked fine. Anything else I should add, do we think?

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

Do we have a reuse existing mpath with partitions in vmtest? If not, that'd be a good thing to add.

~mwhudson/curtin:lp-1893818 updated
0790cfc... by Michael Hudson-Doyle

add a vmtest for reusing partitions of multipath disk

have not managed to actually run this yet

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

I ran the new vmtest but it failed across all the releases:

https://jenkins.ubuntu.com/server/job/curtin-vmtest-devel-debug/222/console

but I don't know if that's really a useful failure, as running the test_multipath.py tests from master also failed:

cmd: nosetests3 --process-timeout=86400 --processes=4 -vv --nologcapture tests/vmtests/test_multipath.py

console log: https://jenkins.ubuntu.com/server/job/curtin-vmtest-devel-debug/221/console
copypaste here: https://paste.ubuntu.com/p/HTp4xh2qYH/

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

Hm thanks, that definitely looks like some kind of problem in the newly added yaml file :/ make sync-images finally completed here so I can run some tests myself now :)

~mwhudson/curtin:lp-1893818 updated
ca8b1c0... by Michael Hudson-Doyle

fix things

9348f08... by Michael Hudson-Doyle

fix things some more

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

I fixed some simple and some obscure things and at least some of the tests pass for me now :) Can you have another whack at in jenkins?

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

pylint is flaky with the usual:

00:12:11.509 ************* Module curtin.commands.install_grub
00:12:11.509 curtin/commands/install_grub.py:93:19: E1101: Instance of 'Distros' has no 'debian' member (no-member)
00:12:11.509 curtin/commands/install_grub.py:155:28: E1101: Instance of 'Distros' has no 'redhat' member (no-member)
00:12:11.509 curtin/commands/install_grub.py:246:28: E1101: Instance of 'Distros' has no 'debian' member (no-member)

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

So the new vmtests passed. Of the old multipath tests, Centos70TestMultipathBasic fails but it also fails the same way on master (like this:

[ 214.243025] cloud-init[1257]: start: cmd-install/stage-late/00_grub1_boot: running 'curtin in-target -- sed -i.curtin -e s|(hd1,0)|(hd0,0)|g /boot/grub/grub.conf'
[ 214.710207] cloud-init[1257]: start: cmd-install/stage-late/00_grub1_boot/cmd-in-target: curtin command in-target
[ 214.718102] cloud-init[1257]: Running command ['mount', '--bind', '/dev', '/tmp/tmp69un0w5l/target/dev'] with allowed return codes [0] (capture=False)
[ 214.726514] cloud-init[1257]: Running command ['mount', '--bind', '/proc', '/tmp/tmp69un0w5l/target/proc'] with allowed return codes [0] (capture=False)
[ 214.734124] cloud-init[1257]: Running command ['mount', '--bind', '/run', '/tmp/tmp69un0w5l/target/run'] with allowed return codes [0] (capture=False)
[ 214.741353] cloud-init[1257]: Running command ['mount', '--bind', '/sys', '/tmp/tmp69un0w5l/target/sys'] with allowed return codes [0] (capture=False)
[ 214.749061] cloud-init[1257]: Running command ['unshare', '--help'] with allowed return codes [0] (capture=True)
[ 214.755668] cloud-init[1257]: Running command ['unshare', '--fork', '--pid', '--', 'chroot', '/tmp/tmp69un0w5l/target', 'sed', '-i.curtin', '-e', 's|(hd1,0)|(hd0,0)|g', '/boot/grub/grub.conf'] with allowed return codes [0] (capture=False)
[ 214.760850] cloud-init[1257]: sed: can't read /boot/grub/grub.conf: No such file or directory
[ 214.764817] cloud-init[1257]: Running command ['udevadm', 'settle'] with allowed return codes [0] (capture=False)
[ 214.784140] cloud-init[1257]: TIMED subp(['udevadm', 'settle']): 0.018
[ 214.785396] cloud-init[1257]: Running command ['umount', '/tmp/tmp69un0w5l/target/sys'] with allowed return codes [0] (capture=False)
[ 214.792526] cloud-init[1257]: Running command ['umount', '/tmp/tmp69un0w5l/target/run'] with allowed return codes [0] (capture=False)
[ 214.799577] cloud-init[1257]: Running command ['umount', '/tmp/tmp69un0w5l/target/proc'] with allowed return codes [0] (capture=False)
[ 214.806709] cloud-init[1257]: Running command ['umount', '/tmp/tmp69un0w5l/target/dev'] with allowed return codes [0] (capture=False)
[ 214.813171] cloud-init[1257]: finish: cmd-install/stage-late/00_grub1_boot/cmd-in-target: FAIL: curtin command in-target
[ 214.858039] cloud-init[1257]: 00_grub1_boot command failed

)

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

Anyway, given that this is not a regression caused by this branch, I think this is OK to merge. It's been so long though that I'm going to leave it 24 hours for any more objections :-)

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

That script is from tests/exampltes/centos_defaults.yaml

It was inplace for dealing with an older grub in centos66... that said, I wonder what's changed in the Centos7X image such that the grub.cfg file isn't present any more...

It master passed on this centos image:

 centos/centos70/amd64/20201222_01/root-tgz

And there's now a new centos7 image

 centos/centos70/amd64/20210210_01/root-tgz

where it fails. I'll look a little deeper into the image.

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

The new maas image moved grub config location. =(

https://code.launchpad.net/~raharper/curtin/+git/curtin/+merge/399028

This fixed the multipath test case for sure. I also plan to see if
I can only run that fix up on centos66 to confirm

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

All the multipath vmtests now pass \o/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/curtin/block/multipath.py b/curtin/block/multipath.py
index e317184..0f9170e 100644
--- a/curtin/block/multipath.py
+++ b/curtin/block/multipath.py
@@ -65,11 +65,10 @@ def is_mpath_device(devpath, info=None):
65def is_mpath_member(devpath, info=None):65def is_mpath_member(devpath, info=None):
66 """ Check if a device is a multipath member (a path), returns boolean. """66 """ Check if a device is a multipath member (a path), returns boolean. """
67 result = False67 result = False
68 try:68 if not info:
69 util.subp(['multipath', '-c', devpath], capture=True)69 info = udev.udevadm_info(devpath)
70 if info.get("DM_MULTIPATH_DEVICE_PATH") == "1":
70 result = True71 result = True
71 except util.ProcessExecutionError:
72 pass
7372
74 LOG.debug('%s is multipath device member? %s', devpath, result)73 LOG.debug('%s is multipath device member? %s', devpath, result)
75 return result74 return result
@@ -81,7 +80,7 @@ def is_mpath_partition(devpath, info=None):
81 if devpath.startswith('/dev/dm-'):80 if devpath.startswith('/dev/dm-'):
82 if not info:81 if not info:
83 info = udev.udevadm_info(devpath)82 info = udev.udevadm_info(devpath)
84 if 'DM_PART' in udev.udevadm_info(devpath):83 if 'DM_PART' in info and 'DM_MPATH' in info:
85 result = True84 result = True
8685
87 LOG.debug("%s is multipath device partition? %s", devpath, result)86 LOG.debug("%s is multipath device partition? %s", devpath, result)
diff --git a/curtin/storage_config.py b/curtin/storage_config.py
index e6c33cc..81b7385 100644
--- a/curtin/storage_config.py
+++ b/curtin/storage_config.py
@@ -7,7 +7,7 @@ import re
7import yaml7import yaml
88
9from curtin.log import LOG9from curtin.log import LOG
10from curtin.block import schemas10from curtin.block import multipath, schemas
11from curtin import config as curtin_config11from curtin import config as curtin_config
12from curtin import util12from curtin import util
1313
@@ -453,72 +453,15 @@ class ProbertParser(object):
453453
454 return None454 return None
455455
456 def is_mpath(self, blockdev):456 def is_mpath_member(self, blockdev):
457 if blockdev.get('DM_MULTIPATH_DEVICE_PATH') == "1":457 return multipath.is_mpath_member(blockdev.get('DEVNAME', ''), blockdev)
458 return True
459458
460 return bool('mpath-' in blockdev.get('DM_UUID', ''))459 def is_mpath_device(self, blockdev):
460 return multipath.is_mpath_device(blockdev.get('DEVNAME', ''), blockdev)
461461
462 def get_mpath_name(self, blockdev):462 def is_mpath_partition(self, blockdev):
463 mpath_data = self.probe_data.get('multipath')463 return multipath.is_mpath_partition(
464 if not mpath_data:464 blockdev.get('DEVNAME', ''), blockdev)
465 return None
466
467 bd_name = blockdev['DEVNAME']
468 if blockdev['DEVTYPE'] == 'partition':
469 bd_name = self.partition_parent_devname(blockdev)
470 bd_name = os.path.basename(bd_name)
471 for path in mpath_data.get('paths', []):
472 if bd_name == path.get('device'):
473 rv = path.get('multipath')
474 return rv
475
476 def find_mpath_member(self, blockdev):
477 if blockdev.get('DM_MULTIPATH_DEVICE_PATH') == "1":
478 # find all other DM_MULTIPATH_DEVICE_PATH devs with same serial
479 serial = blockdev.get('ID_SERIAL')
480 members = sorted([os.path.basename(dev['DEVNAME'])
481 for dev in self.blockdev_data.values()
482 if dev.get("ID_SERIAL", "") == serial and
483 dev['DEVTYPE'] == blockdev['DEVTYPE']])
484 # [/dev/sda, /dev/sdb]
485 # [/dev/sda1, /dev/sda2, /dev/sdb1, /dev/sdb2]
486
487 else:
488 dm_mpath = blockdev.get('DM_MPATH')
489 dm_uuid = blockdev.get('DM_UUID')
490 dm_part = blockdev.get('DM_PART')
491 dm_name = blockdev.get('DM_NAME')
492
493 if dm_mpath:
494 multipath = dm_mpath
495 elif dm_name:
496 multipath = dm_name
497 else:
498 # part1-mpath-30000000000000064
499 # mpath-30000000000000064
500 # mpath-36005076306ffd6b60000000000002406
501 match = re.search(r'mpath\-([a-zA-Z]*|\d*)+$', dm_uuid)
502 if not match:
503 LOG.debug('Failed to extract multipath ID pattern from '
504 'DM_UUID value: "%s"', dm_uuid)
505 return None
506 # remove leading 'mpath-'
507 multipath = match.group(0)[6:]
508 mpath_data = self.probe_data.get('multipath')
509 if not mpath_data:
510 return None
511 members = sorted([path['device'] for path in mpath_data['paths']
512 if path['multipath'] == multipath])
513
514 # append partition number if present
515 if dm_part:
516 members = [member + dm_part for member in members]
517
518 if len(members):
519 return members[0]
520
521 return None
522465
523 def blockdev_to_id(self, blockdev):466 def blockdev_to_id(self, blockdev):
524 """ Examine a blockdev dictionary and return a tuple of curtin467 """ Examine a blockdev dictionary and return a tuple of curtin
@@ -539,15 +482,13 @@ class ProbertParser(object):
539 if 'DM_LV_NAME' in blockdev:482 if 'DM_LV_NAME' in blockdev:
540 devtype = 'lvm-partition'483 devtype = 'lvm-partition'
541 name = blockdev['DM_LV_NAME']484 name = blockdev['DM_LV_NAME']
542 elif self.is_mpath(blockdev):485 elif self.is_mpath_device(blockdev):
543 # extract a multipath member device486 devtype = 'mpath-disk'
544 member = self.find_mpath_member(blockdev)487 name = blockdev['DM_NAME']
545 if member:488 elif self.is_mpath_partition(blockdev):
546 name = member489 devtype = 'mpath-partition'
547 else:490 name = '{}-part{}'.format(
548 name = blockdev['DM_UUID']491 blockdev['DM_MPATH'], blockdev['DM_PART'])
549 if 'DM_PART' in blockdev:
550 devtype = 'partition'
551 elif is_dmcrypt(blockdev):492 elif is_dmcrypt(blockdev):
552 devtype = 'dmcrypt'493 devtype = 'dmcrypt'
553 name = blockdev['DM_NAME']494 name = blockdev['DM_NAME']
@@ -681,10 +622,15 @@ class BlockdevParser(ProbertParser):
681 errors = []622 errors = []
682623
683 for devname, data in self.blockdev_data.items():624 for devname, data in self.blockdev_data.items():
684 # skip composed devices here, except partitions625 # skip composed devices here, except partitions and multipath
685 if data.get('DEVPATH', '').startswith('/devices/virtual/block'):626 if data.get('DEVPATH', '').startswith('/devices/virtual/block'):
686 if data.get('DEVTYPE', '') != "partition":627 if not self.is_mpath_device(data):
687 continue628 if not self.is_mpath_partition(data):
629 if data.get('DEVTYPE', '') != "partition":
630 continue
631 # skip disks that are members of multipath devices
632 if self.is_mpath_member(data):
633 continue
688 entry = self.asdict(data)634 entry = self.asdict(data)
689 if entry:635 if entry:
690 try:636 try:
@@ -698,7 +644,10 @@ class BlockdevParser(ProbertParser):
698 def valid_id(self, id_value):644 def valid_id(self, id_value):
699 # reject wwn=0x0+645 # reject wwn=0x0+
700 if id_value.lower().startswith('0x'):646 if id_value.lower().startswith('0x'):
701 return int(id_value, 16) > 0647 try:
648 return int(id_value, 16) > 0
649 except ValueError:
650 return True
702 # accept non-empty (removing whitspace) strings651 # accept non-empty (removing whitspace) strings
703 return len(''.join(id_value.split())) > 0652 return len(''.join(id_value.split())) > 0
704653
@@ -710,10 +659,16 @@ class BlockdevParser(ProbertParser):
710 blockdev attribute.659 blockdev attribute.
711 """660 """
712 uniq = {}661 uniq = {}
713 source_keys = {662 if self.is_mpath_device(blockdev):
714 'wwn': ['ID_WWN_WITH_EXTENSION', 'ID_WWN'],663 source_keys = {
715 'serial': ['ID_SERIAL', 'ID_SERIAL_SHORT'],664 'wwn': ['DM_WWN'],
716 }665 'serial': ['DM_SERIAL'], # only present with focal+
666 }
667 else:
668 source_keys = {
669 'wwn': ['ID_WWN_WITH_EXTENSION', 'ID_WWN'],
670 'serial': ['ID_SERIAL', 'ID_SERIAL_SHORT'],
671 }
717 for skey, id_keys in source_keys.items():672 for skey, id_keys in source_keys.items():
718 for id_key in id_keys:673 for id_key in id_keys:
719 if id_key in blockdev and skey not in uniq:674 if id_key in blockdev and skey not in uniq:
@@ -740,6 +695,10 @@ class BlockdevParser(ProbertParser):
740 storage config dictionary. This method695 storage config dictionary. This method
741 will return curtin storage types: disk, partition.696 will return curtin storage types: disk, partition.
742 """697 """
698 dev_type = blockdev_data['DEVTYPE']
699 if self.is_mpath_partition(blockdev_data):
700 dev_type = 'partition'
701
743 # just disks and partitions702 # just disks and partitions
744 if blockdev_data['DEVTYPE'] not in ["disk", "partition"]:703 if blockdev_data['DEVTYPE'] not in ["disk", "partition"]:
745 return None704 return None
@@ -752,13 +711,13 @@ class BlockdevParser(ProbertParser):
752711
753 devname = blockdev_data.get('DEVNAME')712 devname = blockdev_data.get('DEVNAME')
754 entry = {713 entry = {
755 'type': blockdev_data['DEVTYPE'],714 'type': dev_type,
756 'id': self.blockdev_to_id(blockdev_data),715 'id': self.blockdev_to_id(blockdev_data),
757 }716 }
758 if blockdev_data.get('DM_MULTIPATH_DEVICE_PATH') == "1":717 if self.is_mpath_device(blockdev_data):
759 mpath_name = self.get_mpath_name(blockdev_data)718 entry['multipath'] = blockdev_data['DM_NAME']
760 if mpath_name:719 elif self.is_mpath_partition(blockdev_data):
761 entry['multipath'] = mpath_name720 entry['multipath'] = blockdev_data['DM_MPATH']
762721
763 # default disks to gpt722 # default disks to gpt
764 if entry['type'] == 'disk':723 if entry['type'] == 'disk':
@@ -796,8 +755,17 @@ class BlockdevParser(ProbertParser):
796755
797 if entry['type'] == 'partition':756 if entry['type'] == 'partition':
798 attrs = blockdev_data['attrs']757 attrs = blockdev_data['attrs']
799 entry['number'] = int(attrs['partition'])758 if self.is_mpath_partition(blockdev_data):
800 parent_devname = self.partition_parent_devname(blockdev_data)759 entry['number'] = int(blockdev_data['DM_PART'])
760 parent_devname = self.lookup_devname(
761 '/dev/mapper/' + blockdev_data['DM_MPATH'])
762 if parent_devname is None:
763 raise ValueError(
764 "Cannot find parent mpath device %s for %s" % (
765 blockdev_data['DM_MPATH'], devname))
766 else:
767 entry['number'] = int(attrs['partition'])
768 parent_devname = self.partition_parent_devname(blockdev_data)
801 parent_blockdev = self.blockdev_data[parent_devname]769 parent_blockdev = self.blockdev_data[parent_devname]
802 if 'ID_PART_TABLE_TYPE' not in parent_blockdev:770 if 'ID_PART_TABLE_TYPE' not in parent_blockdev:
803 # Exclude the fake partition that the kernel creates771 # Exclude the fake partition that the kernel creates
@@ -810,9 +778,7 @@ class BlockdevParser(ProbertParser):
810 if ptable:778 if ptable:
811 part = None779 part = None
812 for pentry in ptable['partitions']:780 for pentry in ptable['partitions']:
813 node = pentry['node']781 if self.lookup_devname(pentry['node']) == devname:
814 node_p = node.replace(parent_devname, '')
815 if node_p.replace('p', '') == attrs['partition']:
816 part = pentry782 part = pentry
817 break783 break
818784
@@ -880,6 +846,9 @@ class FilesystemParser(ProbertParser):
880 errors.append(err)846 errors.append(err)
881 continue847 continue
882848
849 if self.is_mpath_member(blockdev_data):
850 continue
851
883 # no floppy, no cdrom852 # no floppy, no cdrom
884 if blockdev_data['MAJOR'] in ["11", "2"]:853 if blockdev_data['MAJOR'] in ["11", "2"]:
885 continue854 continue
diff --git a/examples/tests/multipath-reuse.yaml b/examples/tests/multipath-reuse.yaml
886new file mode 100644855new file mode 100644
index 0000000..24e193e
--- /dev/null
+++ b/examples/tests/multipath-reuse.yaml
@@ -0,0 +1,61 @@
1
2# The point of this test is to test installing to a existing
3# partitions of a multipathed disk
4
5bucket:
6 - &setup |
7 parted /dev/disk/by-id/dm-name-mpatha --script -- \
8 mklabel msdos \
9 mkpart primary ext4 1GiB 4GiB \
10 mkpart primary ext4 4GiB 5GiB \
11 set 1 boot on
12 udevadm settle
13
14early_commands:
15 00-setup-msdos-ptable: [sh, -exuc, *setup]
16
17install:
18 unmount: disabled
19showtrace: true
20storage:
21 version: 1
22 config:
23 - id: sda
24 type: disk
25 ptable: msdos
26 serial: 'IPR-0 1234567890'
27 name: mpath_a
28 grub_device: true
29 multipath: mpatha
30 path: /dev/disk/by-id/dm-name-mpatha
31 preserve: true
32 - id: sda1
33 type: partition
34 number: 1
35 size: 3GB
36 device: sda
37 flag: boot
38 preserve: true
39 - id: sda2
40 type: partition
41 number: 2
42 size: 1GB
43 device: sda
44 preserve: true
45 - id: sda1_root
46 type: format
47 fstype: ext4
48 volume: sda1
49 - id: sda2_home
50 type: format
51 fstype: ext4
52 volume: sda2
53 - id: sda1_mount
54 type: mount
55 path: /
56 device: sda1_root
57 - id: sda2_mount
58 type: mount
59 path: /home
60 device: sda2_home
61 options: 'defaults,nofail'
diff --git a/tests/unittests/test_block_multipath.py b/tests/unittests/test_block_multipath.py
index 426be56..db767ab 100644
--- a/tests/unittests/test_block_multipath.py
+++ b/tests/unittests/test_block_multipath.py
@@ -1,7 +1,7 @@
1import mock1import mock
22
3from curtin.block import multipath3from curtin.block import multipath
4from .helpers import CiTestCase, raise_pexec_error4from .helpers import CiTestCase
55
66
7# dmsetup uses tabs as separators7# dmsetup uses tabs as separators
@@ -63,23 +63,40 @@ class TestMultipath(CiTestCase):
63 self.m_udev.udevadm_info.return_value = {'DM_UUID': 'lvm-vg-foo-lv1'}63 self.m_udev.udevadm_info.return_value = {'DM_UUID': 'lvm-vg-foo-lv1'}
64 self.assertFalse(multipath.is_mpath_device(self.random_string()))64 self.assertFalse(multipath.is_mpath_device(self.random_string()))
6565
66 def test_is_mpath_member_true(self):
67 """is_mpath_device returns false when DM_UUID doesnt start w/ mpath-"""
68 self.assertTrue(multipath.is_mpath_member(self.random_string()))
69
70 def test_is_mpath_member_false(self):66 def test_is_mpath_member_false(self):
71 """is_mpath_member returns false if 'multipath -c <dev>' exits err."""67 """is_mpath_member returns false if DM_MULTIPATH_DEVICE_PATH is not
72 self.m_subp.side_effect = raise_pexec_error68 present"""
69 self.m_udev.udevadm_info.return_value = {}
70 self.assertFalse(multipath.is_mpath_member(self.random_string()))
71
72 def test_is_mpath_member_false_2(self):
73 """is_mpath_member returns false if DM_MULTIPATH_DEVICE_PATH is not
74 '1'"""
75 self.m_udev.udevadm_info.return_value = {
76 "DM_MULTIPATH_DEVICE_PATH": "2",
77 }
73 self.assertFalse(multipath.is_mpath_member(self.random_string()))78 self.assertFalse(multipath.is_mpath_member(self.random_string()))
7479
80 def test_is_mpath_member_true(self):
81 """is_mpath_member returns true if DM_MULTIPATH_DEVICE_PATH is
82 '1'"""
83 self.m_udev.udevadm_info.return_value = {
84 "DM_MULTIPATH_DEVICE_PATH": "1",
85 }
86 self.assertTrue(multipath.is_mpath_member(self.random_string()))
87
75 def test_is_mpath_partition_true(self):88 def test_is_mpath_partition_true(self):
76 """is_mpath_member returns true if 'multipath -c <dev>' exits 0."""89 """is_mpath_partition returns true if udev info contains right keys."""
77 dm_device = "/dev/dm-" + self.random_string()90 dm_device = "/dev/dm-" + self.random_string()
78 self.m_udev.udevadm_info.return_value = {'DM_PART': '1'}91 self.m_udev.udevadm_info.return_value = {
92 'DM_PART': '1',
93 'DM_MPATH': 'a',
94 }
79 self.assertTrue(multipath.is_mpath_partition(dm_device))95 self.assertTrue(multipath.is_mpath_partition(dm_device))
8096
81 def test_is_mpath_partition_false(self):97 def test_is_mpath_partition_false(self):
82 """is_mpath_member returns false if DM_PART is not present for dev."""98 """is_mpath_partition returns false if DM_PART is not present for dev.
99 """
83 self.assertFalse(multipath.is_mpath_partition(self.random_string()))100 self.assertFalse(multipath.is_mpath_partition(self.random_string()))
84101
85 def test_mpath_partition_to_mpath_id_and_partnumber(self):102 def test_mpath_partition_to_mpath_id_and_partnumber(self):
diff --git a/tests/unittests/test_storage_config.py b/tests/unittests/test_storage_config.py
index 2d27d4e..0ed7659 100644
--- a/tests/unittests/test_storage_config.py
+++ b/tests/unittests/test_storage_config.py
@@ -467,149 +467,51 @@ class TestBlockdevParser(CiTestCase):
467 self.assertDictEqual(expected_dict,467 self.assertDictEqual(expected_dict,
468 self.bdevp.asdict(blockdev))468 self.bdevp.asdict(blockdev))
469469
470 def test_blockdev_detects_multipath(self):470 def test_blockdev_multipath_disk(self):
471 self.probe_data = _get_data('probert_storage_multipath.json')471 self.probe_data = _get_data('probert_storage_multipath.json')
472 self.bdevp = BlockdevParser(self.probe_data)472 self.bdevp = BlockdevParser(self.probe_data)
473 blockdev = self.bdevp.blockdev_data['/dev/sda2']473 blockdev = self.bdevp.blockdev_data['/dev/dm-0']
474 expected_dict = {474 expected_dict = {
475 'flag': 'linux',475 'id': 'mpath-disk-mpatha',
476 'id': 'partition-sda2',
477 'offset': 2097152,
478 'multipath': 'mpatha',476 'multipath': 'mpatha',
479 'size': 10734272512,477 'path': '/dev/dm-0',
480 'type': 'partition',478 'ptable': 'gpt',
481 'device': 'disk-sda',479 'type': 'disk',
482 'number': 2}480 'wwn': '0x0000000000000064',
481 }
483 self.assertDictEqual(expected_dict, self.bdevp.asdict(blockdev))482 self.assertDictEqual(expected_dict, self.bdevp.asdict(blockdev))
484483
485 def test_blockdev_skips_multipath_entry_if_no_multipath_data(self):484 def test_blockdev_multipath_partition(self):
486 self.probe_data = _get_data('probert_storage_multipath.json')485 self.probe_data = _get_data('probert_storage_multipath.json')
487 del self.probe_data['multipath']
488 self.bdevp = BlockdevParser(self.probe_data)486 self.bdevp = BlockdevParser(self.probe_data)
489 blockdev = self.bdevp.blockdev_data['/dev/sda2']487 blockdev = self.bdevp.blockdev_data['/dev/dm-2']
490 expected_dict = {488 expected_dict = {
489 'device': 'mpath-disk-mpatha',
491 'flag': 'linux',490 'flag': 'linux',
492 'id': 'partition-sda2',491 'id': 'mpath-partition-mpatha-part2',
492 'multipath': 'mpatha',
493 'number': 2,
493 'offset': 2097152,494 'offset': 2097152,
494 'size': 10734272512,495 'size': 10734272512,
495 'type': 'partition',496 'type': 'partition',
496 'device': 'disk-sda',497 }
497 'number': 2}
498 self.assertDictEqual(expected_dict, self.bdevp.asdict(blockdev))498 self.assertDictEqual(expected_dict, self.bdevp.asdict(blockdev))
499499
500 def test_blockdev_skips_multipath_entry_if_bad_multipath_data(self):500 @skipUnlessJsonSchema()
501 def test_blockdev_skips_underlying_disks_and_partitions(self):
501 self.probe_data = _get_data('probert_storage_multipath.json')502 self.probe_data = _get_data('probert_storage_multipath.json')
502 for path in self.probe_data['multipath']['paths']:
503 path['multipath'] = ''
504 self.bdevp = BlockdevParser(self.probe_data)503 self.bdevp = BlockdevParser(self.probe_data)
505 blockdev = self.bdevp.blockdev_data['/dev/sda2']504 configs = self.bdevp.parse()[0]
506 expected_dict = {505 config_paths = {c.get('path') for c in configs}
507 'flag': 'linux',506 self.assertNotIn('/dev/sda', config_paths)
508 'id': 'partition-sda2',
509 'offset': 2097152,
510 'size': 10734272512,
511 'type': 'partition',
512 'device': 'disk-sda',
513 'number': 2}
514 self.assertDictEqual(expected_dict, self.bdevp.asdict(blockdev))
515
516 def test_blockdev_skips_multipath_entry_if_no_mp_paths(self):
517 self.probe_data = _get_data('probert_storage_multipath.json')
518 del self.probe_data['multipath']['paths']
519 self.bdevp = BlockdevParser(self.probe_data)
520 blockdev = self.bdevp.blockdev_data['/dev/sda2']
521 expected_dict = {
522 'flag': 'linux',
523 'id': 'partition-sda2',
524 'offset': 2097152,
525 'size': 10734272512,
526 'type': 'partition',
527 'device': 'disk-sda',
528 'number': 2}
529 self.assertDictEqual(expected_dict, self.bdevp.asdict(blockdev))
530507
531 def test_blockdev_finds_multipath_id_from_dm_uuid(self):508 def test_blockdev_finds_multipath_id_from_dm_uuid(self):
532 self.probe_data = _get_data('probert_storage_zlp6.json')509 self.probe_data = _get_data('probert_storage_zlp6.json')
533 self.bdevp = BlockdevParser(self.probe_data)510 self.bdevp = BlockdevParser(self.probe_data)
534 blockdev = self.bdevp.blockdev_data['/dev/dm-2']511 blockdev = self.bdevp.blockdev_data['/dev/dm-2']
535 result = self.bdevp.blockdev_to_id(blockdev)512 result = self.bdevp.blockdev_to_id(blockdev)
536 self.assertEqual('disk-sda', result)513 self.assertEqual(
537514 'mpath-disk-36005076306ffd6b60000000000002406', result)
538 def test_blockdev_find_mpath_members_checks_dm_name(self):
539 """ BlockdevParser find_mpath_members uses dm_name if present."""
540 dm14 = {
541 "DEVTYPE": "disk",
542 "DEVLINKS": "/dev/disk/by-id/dm-name-mpathb",
543 "DEVNAME": "/dev/dm-14",
544 "DEVTYPE": "disk",
545 "DM_NAME": "mpathb",
546 "DM_UUID": "mpath-360050768028211d8b000000000000062",
547 "DM_WWN": "0x60050768028211d8b000000000000062",
548 "MPATH_DEVICE_READY": "1",
549 "MPATH_SBIN_PATH": "/sbin",
550 }
551 multipath = {
552 "maps": [
553 {
554 "multipath": "360050768028211d8b000000000000061",
555 "sysfs": "dm-11",
556 "paths": "4"
557 },
558 {
559 "multipath": "360050768028211d8b000000000000062",
560 "sysfs": "dm-14",
561 "paths": "4"
562 },
563 {
564 "multipath": "360050768028211d8b000000000000063",
565 "sysfs": "dm-15",
566 "paths": "4"
567 }],
568 "paths": [
569 {
570 "device": "sdej",
571 "serial": "0200a084762cXX00",
572 "multipath": "mpatha",
573 "host_wwnn": "0x20000024ff9127de",
574 "target_wwnn": "0x5005076802065e38",
575 "host_wwpn": "0x21000024ff9127de",
576 "target_wwpn": "0x5005076802165e38",
577 "host_adapter": "[undef]"
578 },
579 {
580 "device": "sdel",
581 "serial": "0200a084762cXX00",
582 "multipath": "mpathb",
583 "host_wwnn": "0x20000024ff9127de",
584 "target_wwnn": "0x5005076802065e38",
585 "host_wwpn": "0x21000024ff9127de",
586 "target_wwpn": "0x5005076802165e38",
587 "host_adapter": "[undef]"
588 },
589 {
590 "device": "sdet",
591 "serial": "0200a084762cXX00",
592 "multipath": "mpatha",
593 "host_wwnn": "0x20000024ff9127de",
594 "target_wwnn": "0x5005076802065e37",
595 "host_wwpn": "0x21000024ff9127de",
596 "target_wwpn": "0x5005076802165e37",
597 "host_adapter": "[undef]"
598 },
599 {
600 "device": "sdev",
601 "serial": "0200a084762cXX00",
602 "multipath": "mpathb",
603 "host_wwnn": "0x20000024ff9127de",
604 "target_wwnn": "0x5005076802065e37",
605 "host_wwpn": "0x21000024ff9127de",
606 "target_wwpn": "0x5005076802165e37",
607 "host_adapter": "[undef]"
608 }],
609 }
610 self.bdevp.blockdev_data['/dev/dm-14'] = dm14
611 self.probe_data['multipath'] = multipath
612 self.assertEqual('disk-sdel', self.bdevp.blockdev_to_id(dm14))
613515
614 def test_blockdev_detects_dasd_device_id_and_vtoc_ptable(self):516 def test_blockdev_detects_dasd_device_id_and_vtoc_ptable(self):
615 self.probe_data = _get_data('probert_storage_dasd.json')517 self.probe_data = _get_data('probert_storage_dasd.json')
@@ -1017,22 +919,6 @@ class TestExtractStorageConfig(CiTestCase):
1017 extracted)919 extracted)
1018920
1019 @skipUnlessJsonSchema()921 @skipUnlessJsonSchema()
1020 def test_find_all_multipath(self):
1021 """ verify probed multipath paths are included in config. """
1022 self.probe_data = _get_data('probert_storage_multipath.json')
1023 extracted = storage_config.extract_storage_config(self.probe_data)
1024 config = extracted['storage']['config']
1025 blockdev = self.probe_data['blockdev']
1026
1027 for mpmap in self.probe_data['multipath']['maps']:
1028 nr_disks = int(mpmap['paths'])
1029 mp_name = blockdev['/dev/%s' % mpmap['sysfs']]['DM_NAME']
1030 matched_disks = [cfg for cfg in config
1031 if cfg['type'] == 'disk' and
1032 cfg.get('multipath', '') == mp_name]
1033 self.assertEqual(nr_disks, len(matched_disks))
1034
1035 @skipUnlessJsonSchema()
1036 def test_find_raid_partition(self):922 def test_find_raid_partition(self):
1037 """ verify probed raid partitions are found. """923 """ verify probed raid partitions are found. """
1038 self.probe_data = _get_data('probert_storage_raid1_partitions.json')924 self.probe_data = _get_data('probert_storage_raid1_partitions.json')
@@ -1081,6 +967,19 @@ class TestExtractStorageConfig(CiTestCase):
1081 self.assertEqual(expected_dict, disks[0])967 self.assertEqual(expected_dict, disks[0])
1082968
1083 @skipUnlessJsonSchema()969 @skipUnlessJsonSchema()
970 def test_blockdev_multipath(self):
971 self.probe_data = _get_data('probert_storage_zlp6.json')
972 extracted = storage_config.extract_storage_config(self.probe_data)
973 config = extracted['storage']['config']
974 disks = [cfg for cfg in config if cfg['type'] == 'disk']
975 expected_count = len([
976 1 for bd_name, bd_data in self.probe_data['blockdev'].items()
977 if bd_data.get('DM_UUID', '').startswith('mpath-')
978 or bd_name.startswith('/dev/dasd') and bd_data['DEVTYPE'] == 'disk'
979 ])
980 self.assertEqual(expected_count, len(disks))
981
982 @skipUnlessJsonSchema()
1084 def test_blockdev_skips_invalid_wwn(self):983 def test_blockdev_skips_invalid_wwn(self):
1085 self.probe_data = _get_data('probert_storage_bogus_wwn.json')984 self.probe_data = _get_data('probert_storage_bogus_wwn.json')
1086 extracted = storage_config.extract_storage_config(self.probe_data)985 extracted = storage_config.extract_storage_config(self.probe_data)
diff --git a/tests/vmtests/test_multipath.py b/tests/vmtests/test_multipath.py
index f924d25..163b73a 100644
--- a/tests/vmtests/test_multipath.py
+++ b/tests/vmtests/test_multipath.py
@@ -170,4 +170,20 @@ class GroovyTestMultipathBasic(relbase.groovy, TestMultipathBasicAbs):
170 __test__ = True170 __test__ = True
171171
172172
173class TestMultipathReuseAbs(TestMultipathBasicAbs):
174 conf_file = "examples/tests/multipath-reuse.yaml"
175
176
177class FocalTestMultipathReuse(relbase.focal, TestMultipathReuseAbs):
178 __test__ = True
179
180
181class HirsuteTestMultipathReuse(relbase.hirsute, TestMultipathReuseAbs):
182 __test__ = True
183
184
185class GroovyTestMultipathReuse(relbase.groovy, TestMultipathReuseAbs):
186 __test__ = True
187
188
173# vi: ts=4 expandtab syntax=python189# vi: ts=4 expandtab syntax=python

Subscribers

People subscribed via source and target branches