From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 393FA1FF0AF for ; Fri, 06 Nov 2026 01:52:24 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 77DC52186D; Fri, 06 Nov 2026 01:51:00 +0100 (CET) From: Thomas Lamprecht To: pve-devel@lists.proxmox.com Subject: [PATCH v3 storage 09/76] lvm: refuse to create a volume group on a device that holds data Date: Fri, 6 Nov 2026 00:30:44 +0100 Message-ID: <20261105233336.2178064-10-t.lamprecht@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261105233336.2178064-1-t.lamprecht@proxmox.com> References: <20261105233336.2178064-1-t.lamprecht@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1793926177840 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.054 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) KAM_LOTSOFHASH 0.25 Emails with lots of hash-like gibberish POISEN_SPAM_PILL 0.1 Meta: its spam POISEN_SPAM_PILL_1 0.1 random spam to be learned in bayes POISEN_SPAM_PILL_3 0.1 random spam to be learned in bayes RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: BWSROUPQDKJNSAL6ZFNELNOTASZQ4OML X-Message-ID-Hash: BWSROUPQDKJNSAL6ZFNELNOTASZQ4OML X-MailFrom: t.lamprecht@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Base-volume initialization must not destroy existing file systems, partition tables, or volume groups. Probe immediately before writing with bounded commands, refusing creation if a probe fails. Read PV membership without LVM hints and require explicit evidence that a PV is orphaned. Missing group metadata, including the unknown-name sentinel, is not enough. A PV already in the requested group counts as created; a confirmed orphan left by a failed attempt can be initialized again. Signed-off-by: Thomas Lamprecht --- src/PVE/Diskmanage.pm | 16 ++ src/PVE/Storage/LVMPlugin.pm | 106 +++++++- src/test/Makefile | 5 +- src/test/lvm-fixtures/README.md | 20 ++ .../lab1-nomda-orphan-derived.colon | 2 + src/test/lvm-fixtures/lab1-nomda-orphan.txt | 23 ++ src/test/run_test_lvm_create.pl | 243 ++++++++++++++++++ 7 files changed, 407 insertions(+), 8 deletions(-) create mode 100644 src/test/lvm-fixtures/README.md create mode 100644 src/test/lvm-fixtures/lab1-nomda-orphan-derived.colon create mode 100644 src/test/lvm-fixtures/lab1-nomda-orphan.txt create mode 100755 src/test/run_test_lvm_create.pl diff --git a/src/PVE/Diskmanage.pm b/src/PVE/Diskmanage.pm index d5bd1473..4185d2f7 100644 --- a/src/PVE/Diskmanage.pm +++ b/src/PVE/Diskmanage.pm @@ -73,6 +73,22 @@ sub init_disk { return 1; } +sub parse_wipefs_signatures { + my ($json) = @_; + + my $probe = decode_json($json); + die "Malformed wipefs output\n" + if ref($probe) ne 'HASH' || ref($probe->{signatures}) ne 'ARRAY'; + for my $signature ($probe->{signatures}->@*) { + die "Malformed wipefs signature\n" + if ref($signature) ne 'HASH' + || !defined($signature->{type}) + || ref($signature->{type}) + || $signature->{type} !~ m/^[a-zA-Z0-9_.+-]+$/; + } + return $probe->{signatures}; +} + sub disk_is_used { my ($disk) = @_; diff --git a/src/PVE/Storage/LVMPlugin.pm b/src/PVE/Storage/LVMPlugin.pm index eb9806b2..26908d7e 100644 --- a/src/PVE/Storage/LVMPlugin.pm +++ b/src/PVE/Storage/LVMPlugin.pm @@ -11,6 +11,7 @@ use JSON; use List::Util qw(max); use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC); +use PVE::Diskmanage; use PVE::Exception qw(raise_param_exc); use PVE::Format qw(render_bytes render_duration); use PVE::INotify; @@ -55,25 +56,36 @@ my sub fork_cleanup_worker { } sub lvm_pv_info { - my ($device) = @_; + my ($device, $require_pv) = @_; die "no device specified" if !$device; my $has_label = 0; + my $file_result; - my $cmd = ['/usr/bin/file', '-L', '-s', $device]; + my $cmd = ['/usr/bin/file', '-E', '-L', '-s', $device]; run_command( $cmd, + timeout => 10, outfunc => sub { my $line = shift; + die "unable to probe '$device' - malformed file output\n" + if defined($file_result) || $line !~ m/^\Q$device\E:\s+\S/; + $file_result = $line; $has_label = 1 if $line =~ m/LVM2/; }, ); + die "unable to probe '$device' - empty file output\n" if !defined($file_result); - return undef if !$has_label; + return undef if !$has_label && !$require_pv; $cmd = [ '/sbin/pvs', + '--nohints', + '--readonly', + '--devices', + $device, + '--binary', '--separator', ':', '--noheadings', @@ -82,19 +94,30 @@ sub lvm_pv_info { '--unbuffered', '--nosuffix', '--options', - 'pv_name,pv_size,vg_name,pv_uuid', + 'pv_name,pv_size,vg_name,pv_uuid,pv_in_use', $device, ]; my $pvinfo; run_command( $cmd, + timeout => 10, outfunc => sub { my $line = shift; $line = trim($line); - my ($pvname, $size, $vgname, $uuid) = split(':', $line); + my ($pvname, $size, $vgname, $uuid, $in_use, @extra) = split(':', $line, -1); + # LVM names a device-mapper device by its /dev/mapper link, not the dm node + my $listed = defined($pvname) ? abs_path($pvname) // $pvname : ''; + die "unable to probe '$device' - malformed pvs output\n" + if @extra + || !defined($in_use) + || $in_use !~ m/^(?:0|1|unknown|)$/ + || $listed ne (abs_path($device) // $device) + || $size !~ m/^\d+(?:\.\d+)?$/ + || ($vgname ne '[unknown]' && $vgname !~ m/^[a-zA-Z0-9+_.-]*$/) + || $uuid !~ m/^[a-zA-Z0-9]{6}(?:-[a-zA-Z0-9]{4}){5}-[a-zA-Z0-9]{6}$/; die "found multiple pvs entries for device '$device'\n" if $pvinfo; @@ -104,10 +127,12 @@ sub lvm_pv_info { size => int($size), vgname => $vgname, uuid => $uuid, + in_use => $in_use, }; }, ); + die "unable to probe '$device' - no PV found\n" if !$pvinfo; return $pvinfo; } @@ -124,13 +149,80 @@ sub clear_first_sector { sub lvm_create_volume_group { my ($device, $vgname, $shared) = @_; - my $res = lvm_pv_info($device); + my $requested_device = $device; + $device = PVE::Diskmanage::verify_blockdev_path($device); + my $refuse_data = sub { + my (@types) = @_; + die "device '$device' contains data (" + . join(', ', sort @types) + . ") - wipe it first if it is no longer needed, for example with 'pvesh set /nodes/" + . PVE::INotify::nodename() + . "/disks/wipedisk --disk $requested_device'\n"; + }; - if ($res->{vgname}) { + my $sysdir = '/sys/class/block/' . basename($device); + my $uuid = file_read_firstline("$sysdir/dm/uuid"); + my $mounted = PVE::Diskmanage::is_mounted($device); + die "unable to create volume group - '$mounted' is mounted\n" if $mounted; + if (my $held = PVE::Diskmanage::has_holder($device)) { + # kpartx maps the partitions of a device-mapper device as its holders. Such holders indicate + # a partition table, not a user. + my @holders; + PVE::Tools::dir_glob_foreach("$sysdir/holders", '[^.].*', sub { push @holders, $_[0] }); + my $partitions = defined($uuid) && @holders && !grep { + (file_read_firstline("/sys/class/block/$_/dm/uuid") // '') !~ m/^part\d+-\Q$uuid\E$/ + } @holders; + $refuse_data->('partitions') if $partitions; + die "unable to create volume group - another block device uses '$held'\n"; + } + + my $size = file_read_firstline("$sysdir/size"); + die "unable to probe '$device' - device is unavailable\n" + if !defined($size) || $size !~ m/^\d+$/ || !$size; + die "unable to probe '$device' - missing device mapper identity\n" + if basename($device) =~ m/^dm-\d+$/ && !defined($uuid); + if (defined($uuid) && $uuid =~ m/^mpath-/) { + # A queueing map without a running path can stall even a read-only probe. + my $running = 0; + PVE::Tools::dir_glob_foreach( + "$sysdir/slaves", + '[^.].*', + sub { + my $state = file_read_firstline("$sysdir/slaves/$_[0]/device/state"); + $running = 1 if defined($state) && $state eq 'running'; + }, + ); + die "unable to probe '$device' - multipath map has no running path\n" if !$running; + } + + my $output = ''; + run_command( + ['wipefs', '--no-act', '--json', $device], + timeout => 10, + outfunc => sub { $output .= "$_[0]\n" }, + ); + my $probe = eval { PVE::Diskmanage::parse_wipefs_signatures($output) }; + if (my $err = $@) { + my $kind = $err eq "Malformed wipefs signature\n" ? 'signature' : 'output'; + die "unable to probe '$device' - malformed wipefs $kind\n"; + } + my %signatures = map { $_->{type} => 1 } $probe->@*; + my $res = lvm_pv_info($device, $signatures{LVM2_member}); + + # A PV without metadata copies can belong to a VG whose other devices are not visible. The + # sentinel is not a VG name or orphan proof, even if the use flag contradicts it. + die "device '$device' belongs to a volume group whose metadata is not visible on this node\n" + if $res + && ($res->{vgname} eq '[unknown]' || (!$res->{vgname} && $res->{in_use} ne '0')); + + if ($res && $res->{vgname}) { return if $res->{vgname} eq $vgname; # already created die "device '$device' is already used by volume group '$res->{vgname}'\n"; } + delete $signatures{LVM2_member} if $res && !$res->{vgname}; + $refuse_data->(keys %signatures) if %signatures; + clear_first_sector($device); # else pvcreate fails # we use --metadatasize 250k, which reseults in "pe_start = 512" diff --git a/src/test/Makefile b/src/test/Makefile index 24abdebc..1cc8f00e 100644 --- a/src/test/Makefile +++ b/src/test/Makefile @@ -1,6 +1,6 @@ all: test -test: test_iscsi test_zfspoolplugin test_lvmplugin test_lvm_lock_retry test_lvm_base_volume test_lvm_hints test_lvm_status test_status_command test_disklist test_bwlimit test_plugin test_ovf test_volume_access test_san_libraries +test: test_iscsi test_zfspoolplugin test_lvmplugin test_lvm_create test_lvm_lock_retry test_lvm_base_volume test_lvm_hints test_lvm_status test_status_command test_disklist test_bwlimit test_plugin test_ovf test_volume_access test_san_libraries test_zfspoolplugin: run_test_zfspoolplugin.pl ./run_test_zfspoolplugin.pl @@ -9,6 +9,9 @@ test_lvmplugin: run_test_lvmplugin.pl run_test_lvm_throughput.pl ./run_test_lvmplugin.pl ./run_test_lvm_throughput.pl +test_lvm_create: run_test_lvm_create.pl + ./run_test_lvm_create.pl + test_lvm_lock_retry: run_test_lvm_lock_retry.pl ./run_test_lvm_lock_retry.pl diff --git a/src/test/lvm-fixtures/README.md b/src/test/lvm-fixtures/README.md new file mode 100644 index 00000000..d024eacf --- /dev/null +++ b/src/test/lvm-fixtures/README.md @@ -0,0 +1,20 @@ +# LVM missing-metadata fixtures + +`lab1-nomda-orphan.txt` is the unchanged lvm2 2.03.31 lab capture from +`netapp-lab-20ee/drills/lvm-fixtures/lab1-nomda-orphan.txt`. It includes a genuine orphan and a PV +with no metadata copies whose metadata-bearing peer is not visible on the observing node. + +The wipe and scan tests extract the JSON between `== devices B,C` and `== full nohints` without +rewriting it. Both log warnings remain in the command output supplied to the parsers. The capture +also contains a full-report row summary and hinted output; those sections are preserved for +provenance, not passed to the JSON parsers. + +`lab1-nomda-orphan-derived.colon` is a derived create-probe fixture, not captured command output. +It preserves the device names, VG names, UUIDs, and binary use flags from the captured JSON. The +`pv_size` column is a synthetic 1024 KiB because the captured command did not request a size. +Each line represents a separate device-restricted create probe, not a combined command response. + +These fixtures establish the `[unknown]` VG spelling with use flag `1` and an empty VG with use +flag `0`. Contradictory flags, disappeared aliases, duplicate UUIDs, and malformed fields use +synthetic tests. Neither additional VG sentinels nor a `[unknown]` device name are established by +this capture. diff --git a/src/test/lvm-fixtures/lab1-nomda-orphan-derived.colon b/src/test/lvm-fixtures/lab1-nomda-orphan-derived.colon new file mode 100644 index 00000000..44fb154e --- /dev/null +++ b/src/test/lvm-fixtures/lab1-nomda-orphan-derived.colon @@ -0,0 +1,2 @@ +/dev/mapper/360014056dbd1054071a4de3bd8460ae6:1024::YNV8yf-k7Ty-yCya-Jt9U-9Q5P-OMgB-EviHBt:0 +/dev/mapper/36001405c5a6706aeb1b40c89ba3363eb:1024:[unknown]:EqLIyc-SBrd-vjIE-TB66-ZRRA-JXvu-yHq011:1 diff --git a/src/test/lvm-fixtures/lab1-nomda-orphan.txt b/src/test/lvm-fixtures/lab1-nomda-orphan.txt new file mode 100644 index 00000000..de1cd259 --- /dev/null +++ b/src/test/lvm-fixtures/lab1-nomda-orphan.txt @@ -0,0 +1,23 @@ +== devices B,C + { + "report": [ + { + "pv": [ + {"pv_name":"/dev/mapper/360014056dbd1054071a4de3bd8460ae6", "vg_name":"", "pv_uuid":"YNV8yf-k7Ty-yCya-Jt9U-9Q5P-OMgB-EviHBt", "pv_in_use":"0"}, + {"pv_name":"/dev/mapper/36001405c5a6706aeb1b40c89ba3363eb", "vg_name":"[unknown]", "pv_uuid":"EqLIyc-SBrd-vjIE-TB66-ZRRA-JXvu-yHq011", "pv_in_use":"1"} + ] + } + ] + , + "log": [ + {"log_seq_num":"1", "log_type":"warn", "log_context":"processing", "log_object_type":"orphan", "log_object_name":"lvm2", "log_object_id":"", "log_object_group":"", "log_object_group_id":"", "log_message":"WARNING: PV /dev/mapper/36001405c5a6706aeb1b40c89ba3363eb is marked in use but no VG was found using it.", "log_errno":"0", "log_ret_code":"0"}, + {"log_seq_num":"2", "log_type":"warn", "log_context":"processing", "log_object_type":"orphan", "log_object_name":"lvm2", "log_object_id":"", "log_object_group":"", "log_object_group_id":"", "log_message":"WARNING: PV /dev/mapper/36001405c5a6706aeb1b40c89ba3363eb might need repairing.", "log_errno":"0", "log_ret_code":"0"} + ] + } +== full nohints (B, C rows) +[{'pv_name': '/dev/mapper/360014056dbd1054071a4de3bd8460ae6', 'vg_name': '', 'pv_uuid': 'YNV8yf-k7Ty-yCya-Jt9U-9Q5P-OMgB-EviHBt', 'pv_in_use': '0'}, {'pv_name': '/dev/mapper/36001405c5a6706aeb1b40c89ba3363eb', 'vg_name': '[unknown]', 'pv_uuid': 'EqLIyc-SBrd-vjIE-TB66-ZRRA-JXvu-yHq011', 'pv_in_use': '1'}] + +== hinted B + WARNING: PV /dev/mapper/36001405c5a6706aeb1b40c89ba3363eb is marked in use but no VG was found using it. + WARNING: PV /dev/mapper/36001405c5a6706aeb1b40c89ba3363eb might need repairing. + /dev/mapper/36001405c5a6706aeb1b40c89ba3363eb [unknown] used diff --git a/src/test/run_test_lvm_create.pl b/src/test/run_test_lvm_create.pl new file mode 100755 index 00000000..2982cda4 --- /dev/null +++ b/src/test/run_test_lvm_create.pl @@ -0,0 +1,243 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use FindBin qw($Bin); +use JSON; +use Test::MockModule; +use Test::More; + +use lib ('.', '..'); +use PVE::Storage::LVMPlugin; + +my $lvm = Test::MockModule->new('PVE::Storage::LVMPlugin'); +my $disk = Test::MockModule->new('PVE::Diskmanage'); +my $tools = Test::MockModule->new('PVE::Tools'); +my $inotify = Test::MockModule->new('PVE::INotify'); +$inotify->mock(nodename => sub { 'testnode' }); +my ($mounted, $held, $map, $running, $size, $signatures, $label, $pv, $fail, $bad_json); +my ($holder_uuid, $pv_alias); +my @writes; +my @probes; +$disk->mock( + verify_blockdev_path => sub { '/dev/sda' }, + is_mounted => sub { $mounted }, + has_holder => sub { $held }, +); +$tools->mock(dir_glob_foreach => sub { $_[2]->($_[0] =~ m{/holders$} ? 'dm-5' : 'sdb') }); +$lvm->mock( + file_read_firstline => sub { + return $size if $_[0] =~ m{/size$}; + return $holder_uuid if $_[0] =~ m{/dm-5/dm/uuid$}; + return $map ? 'mpath-3600abc' : undef if $_[0] =~ m{/uuid$}; + return $running ? 'running' : 'offline' if $_[0] =~ m{/state$}; + die "unexpected read $_[0]"; + }, + clear_first_sector => sub { push @writes, 'clear' }, + # pvs lists a multipath map by its /dev/mapper link + abs_path => sub { $_[0] eq $pv_alias ? '/dev/sda' : $_[0] }, + run_command => sub { + my ($cmd, %opt) = @_; + my $tool = $cmd->[0]; + if ($tool =~ m{(?:file|pvs|wipefs)$}) { + push @probes, $tool; + is($opt{timeout}, 10, "$tool has a timeout"); + die "$tool failed or timed out\n" if ($fail // '') eq $tool; + if ($tool eq 'wipefs') { + $opt{outfunc}->($bad_json // encode_json({ signatures => $signatures })); + } elsif ($tool eq '/usr/bin/file') { + $opt{outfunc}->($label); + } else { + is_deeply( + [@$cmd[1 .. 6]], + [ + '--nohints', + '--readonly', + '--devices', + '/dev/sda', + '--binary', + '--separator', + ], + 'PV lookup reads the candidate without hints or activation', + ); + ok( + grep($_ eq 'pv_name,pv_size,vg_name,pv_uuid,pv_in_use', @$cmd), + 'PV use requested', + ); + $opt{outfunc}->($pv) if defined($pv); + } + } else { + push @writes, $tool; + } + }, +); + +my sub reset_case { + ($mounted, $held, $map, $running, $size) = (undef, undef, 0, 1, 1024); + $signatures = []; + $label = '/dev/sda: data'; + $pv = undef; + $pv_alias = '/dev/mapper/3600abc'; + $fail = undef; + $bad_json = undef; + @writes = (); + @probes = (); +} + +my sub create { + eval { PVE::Storage::LVMPlugin::lvm_create_volume_group('/dev/disk/by-id/test', 'test', 1) }; + return $@; +} + +reset_case(); +is(create(), '', 'blank device accepted'); +is_deeply(\@writes, ['clear', '/sbin/pvcreate', '/sbin/vgcreate'], 'writes only after probes'); + +for my $case ( + ['mount', sub { $mounted = '/dev/sda1' }, qr/is mounted/], + ['holder', sub { $held = '/dev/sda' }, qr{another block device uses '/dev/sda'}], + ['unavailable', sub { $size = 0 }, qr/unavailable/], + ['lost paths', sub { $map = 1; $running = 0 }, qr/no running path/], +) { + reset_case(); + $case->[1]->(); + like(create(), $case->[2], "$case->[0] refused"); + is_deeply(\@probes, [], "$case->[0]: no device reads"); + is_deeply(\@writes, [], "$case->[0]: no writes"); +} + +reset_case(); +$map = 1; +is(create(), '', 'map with a running slave accepted'); + +reset_case(); +$signatures = [{ type => 'gpt' }, { type => 'ext4' }]; +is( + create(), + "device '/dev/sda' contains data (ext4, gpt) - wipe it first if it is no longer needed," + . " for example with 'pvesh set /nodes/testnode/disks/wipedisk --disk /dev/disk/by-id/test'\n", + 'refusal names signatures, local node, and the caller device reference', +); +is_deeply(\@writes, [], 'signatures never overwritten'); + +my $uuid = 'aaaaaa-bbbb-cccc-dddd-eeee-ffff-gggggg'; +for my $vg ('test', 'other', 'unknown', '') { + reset_case(); + $signatures = [{ type => 'LVM2_member' }]; + $label = '/dev/sda: LVM2 PV'; + $pv = "/dev/sda:1024:$vg:$uuid:" . ($vg ? '1' : '0'); + if ($vg && $vg ne 'test') { + like(create(), qr/already used by volume group/, 'foreign VG refused'); + } else { + is(create(), '', $vg ? 'same VG accepted' : 'orphan PV accepted'); + } + is(scalar(@writes), $vg ? 0 : 3, 'only the orphan PV is initialized'); +} + +for my $vg ('', '[unknown]') { + for my $used ('1', '', 'unknown', $vg ? ('0') : ()) { + reset_case(); + $signatures = [{ type => 'LVM2_member' }]; + $pv = "/dev/mapper/3600abc:1024:$vg:$uuid:$used"; + like( + create(), + qr/belongs to a volume group whose metadata is not visible/, + "VG '$vg' with use flag '$used' refused before named-VG handling", + ); + is_deeply(\@writes, [], 'uncertain orphan never initialized'); + } +} +reset_case(); +$signatures = [{ type => 'LVM2_member' }]; +$pv = "/dev/sda:1024:[unknown]:$uuid:1"; +eval { PVE::Storage::LVMPlugin::lvm_create_volume_group('/dev/sda', '[unknown]', 1) }; +like($@, qr/metadata is not visible/, 'sentinel cannot match the requested VG as already created'); +is_deeply(\@writes, [], 'sentinel match causes no writes'); + +subtest 'derived colon-format no-metadata PV and orphan probes' => sub { + my $fixture = PVE::Tools::file_get_contents( + "$Bin/lvm-fixtures/lab1-nomda-orphan-derived.colon", + ); + my @lines = split('\n', $fixture); + is(scalar(@lines), 2, 'derived fixture contains one probe for each captured PV'); + for my $line (@lines) { + reset_case(); + $signatures = [{ type => 'LVM2_member' }]; + $label = '/dev/sda: LVM2 PV'; + $pv = $line; + my @fields = split(':', $line, -1); + $pv_alias = $fields[0]; + if ($fields[2] eq '[unknown]') { + like( + create(), + qr/belongs to a volume group whose metadata is not visible/, + 'derived no-metadata probe receives the specific refusal', + ); + is_deeply(\@writes, [], 'derived no-metadata probe causes no writes'); + } else { + is(create(), '', 'derived genuine orphan probe permits creation'); + is_deeply( + \@writes, + ['clear', '/sbin/pvcreate', '/sbin/vgcreate'], + 'only the derived genuine orphan reaches the mocked writes', + ); + } + } +}; + +# kpartx maps the partitions of a map as its holders, which indicates a partition table +reset_case(); +($map, $held, $holder_uuid) = (1, '/dev/sda', 'part1-mpath-3600abc'); +like( + create(), + qr{contains data \(partitions\) - wipe it first .* --disk /dev/disk/by-id/test'}, + 'partition maps of the map refused as data', +); +is_deeply(\@writes, [], 'partition maps: no writes'); +($map, $held, $holder_uuid) = (1, '/dev/sda', 'LVM-otherlv'); +like(create(), qr{another block device uses '/dev/sda'}, 'another holder refused as a user'); + +reset_case(); +$signatures = [{ type => 'LVM2_member' }]; +$label = '/dev/sda: LVM2 PV'; +$pv = "/dev/mapper/3600abc:1024:test:$uuid:1"; +is(create(), '', 'same VG on a map listed by its mapper name accepted'); +is(scalar(@writes), 0, 'same VG on a map: no writes'); + +for my $case ( + ['missing PV', sub { $pv = undef }, qr/no PV found/], + ['wrong PV', sub { $pv = "/dev/sdb:1024::$uuid:0" }, qr/malformed pvs/], + ['bad PV size', sub { $pv = "/dev/sda:nan::$uuid:0" }, qr/malformed pvs/], + ['missing PV use', sub { $pv = "/dev/sda:1024::$uuid" }, qr/malformed pvs/], + ['invalid PV use', sub { $pv = "/dev/sda:1024::$uuid:maybe" }, qr/malformed pvs/], + ['sentinel missing use', sub { $pv = "/dev/sda:1024:[unknown]:$uuid" }, qr/malformed pvs/], + [ + 'sentinel invalid use', + sub { $pv = "/dev/sda:1024:[unknown]:$uuid:maybe" }, + qr/malformed pvs/, + ], + ['invalid VG sentinel', sub { $pv = "/dev/sda:1024:[missing]:$uuid:0" }, qr/malformed pvs/], + ['missing device', sub { $pv = "[unknown]:1024:[unknown]:$uuid:1" }, qr/malformed pvs/], + [ + 'extra signature', + sub { push @$signatures, { type => 'gpt' } }, + qr/contains data \(gpt\)/, + ], + ['bad file', sub { $label = 'garbage' }, qr/malformed file/], + ['bad JSON', sub { $bad_json = '{}' }, qr/malformed wipefs/], + ['bad signature', sub { $signatures = [{}] }, qr/malformed wipefs/], + map { + my $tool = $_; + ["failed $tool", sub { $fail = $tool }, qr/failed or timed out/] + } ('wipefs', '/usr/bin/file', '/sbin/pvs'), +) { + reset_case(); + $signatures = [{ type => 'LVM2_member' }]; + $pv = "/dev/sda:1024::$uuid:0"; + $case->[1]->(); + like(create(), $case->[2], "$case->[0] refused"); + is_deeply(\@writes, [], "$case->[0]: no writes"); +} + +done_testing(); -- 2.47.3