public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Thomas Lamprecht <t.lamprecht@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH v3 storage 25/76] diskmanage: collect local block device users
Date: Fri,  6 Nov 2026 00:31:00 +0100	[thread overview]
Message-ID: <20261105233336.2178064-26-t.lamprecht@proxmox.com> (raw)
In-Reply-To: <20261105233336.2178064-1-t.lamprecht@proxmox.com>

Destructive operations must reject devices with local users, but each
caller checked only some forms of use. Share a check covering holders
recursively, partitions, mounts, swap, loop and btrfs devices, LIO and
nvmet exports, and processes with the device open. Failed probes are
errors, not evidence that a device is unused.

Run this check before each SAN volume-group initialization write through
the disk API and storage-add base-volume path. A retry for the same
volume group remains write-free. LVM-thin keeps its existing policy.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---
 src/PVE/Diskmanage.pm                    | 147 +++++++++++++++
 src/PVE/Storage/LVMPlugin.pm             |  17 +-
 src/test/Makefile                        |   6 +-
 src/test/disk_api_test.pm                |  26 ---
 src/test/run_block_device_users_tests.pl | 134 +++++++++++++
 src/test/run_san_lvm_create_tests.pl     | 230 +++++++++++++++++++++++
 6 files changed, 531 insertions(+), 29 deletions(-)
 create mode 100755 src/test/run_block_device_users_tests.pl
 create mode 100755 src/test/run_san_lvm_create_tests.pl

diff --git a/src/PVE/Diskmanage.pm b/src/PVE/Diskmanage.pm
index a7eb2b21..655cee37 100644
--- a/src/PVE/Diskmanage.pm
+++ b/src/PVE/Diskmanage.pm
@@ -6,6 +6,7 @@ use warnings;
 use PVE::ProcFSTools;
 use Data::Dumper;
 use Cwd qw(abs_path);
+use Errno qw(ENOENT ESRCH);
 use Fcntl ':mode';
 use File::Basename;
 use File::stat;
@@ -59,6 +60,152 @@ sub assert_blockdev {
     return 1;
 }
 
+# Device numbers also identify aliases and open descriptors after a device node was renamed.
+sub block_device_number {
+    my ($path) = @_;
+
+    my @st = CORE::stat($path);
+    die "unable to stat '$path' - $!\n" if !@st;
+    return undef if !S_ISBLK($st[2]);
+    return PVE::Tools::dev_t_major($st[6]) . ':' . PVE::Tools::dev_t_minor($st[6]);
+}
+
+sub block_device_users {
+    my ($devices, $options) = @_;
+    $options //= {};
+    my $sys = $options->{sysfs} // '/sys';
+    my $proc = $options->{procfs} // '/proc';
+
+    my $entries = sub {
+        my ($dir, $optional) = @_;
+        if (!opendir(my $dh, $dir)) {
+            return [] if $optional && ($! == ENOENT || $! == ESRCH);
+            die "unable to list '$dir' - $!\n";
+        } else {
+            my @names = sort grep { $_ ne '.' && $_ ne '..' } readdir($dh);
+            closedir($dh) or die "unable to close '$dir' - $!\n";
+            return \@names;
+        }
+    };
+    my $read = sub {
+        my ($file) = @_;
+        my $value = file_read_firstline($file);
+        die "unable to read '$file' - missing value\n" if !defined($value);
+        return $value;
+    };
+    my ($numbers, $seen, $users) = ({}, {}, []);
+    my $visit;
+    $visit = sub {
+        my ($name) = @_;
+        die "invalid block device name '$name'\n" if $name !~ m/^[a-zA-Z0-9_.!-]+$/;
+        return if $seen->{$name}++;
+        my $dir = "$sys/class/block/$name";
+        my $dev = $read->("$dir/dev");
+        die "invalid device number for '$name'\n" if $dev !~ m/^\d+:\d+$/;
+        $numbers->{$dev} = $name;
+        for my $part ($entries->($dir)->@*) {
+            next if !-e "$dir/$part/partition";
+            my $number = $read->("$dir/$part/partition");
+            die "invalid partition number for '$part'\n" if $number !~ m/^[1-9]\d*$/;
+            $visit->($part);
+        }
+        for my $holder ($entries->("$dir/holders")->@*) {
+            push $users->@*, "$name held by $holder";
+            $visit->($holder);
+        }
+    };
+    $visit->($_) for sort $devices->@*;
+    undef $visit;
+    return $users if !%$numbers;
+
+    my $mountinfo = file_get_contents("$proc/self/mountinfo");
+    die "unable to check mounts - empty mountinfo\n" if !$mountinfo;
+    for my $line (split(/\n/, $mountinfo)) {
+        my (undef, undef, $dev, undef, $target) = split(/\s+/, $line);
+        die "unable to check mounts - malformed mountinfo\n"
+            if !defined($target) || $dev !~ m/^\d+:\d+$/ || $line !~ m/ - /;
+        push $users->@*, "$numbers->{$dev} mounted at $target" if defined($numbers->{$dev});
+    }
+
+    my $by_path = sub {
+        my ($path, $what) = @_;
+        return if $path eq '';
+        $path =~ s/\\([0-7]{3})/chr(oct($1))/ge;
+        my $dev = block_device_number($path);
+        push $users->@*, "$numbers->{$dev} $what"
+            if defined($dev) && defined($numbers->{$dev});
+    };
+    my $swaps = file_get_contents("$proc/swaps");
+    die "unable to check swap - malformed swaps\n" if $swaps !~ s/^Filename\s+[^\n]*\n//;
+    for my $line (split(/\n/, $swaps)) {
+        my ($path) = $line =~ m/^(\/\S+)\s+/;
+        die "unable to check swap - malformed entry\n" if !defined($path);
+        $by_path->($path, 'used as swap');
+    }
+
+    for my $loop (grep { /^loop\d+$/ } $entries->("$sys/class/block")->@*) {
+        next if !-d "$sys/class/block/$loop/loop";
+        $by_path->($read->("$sys/class/block/$loop/loop/backing_file"), "backs $loop");
+    }
+    for my $fsid ($entries->("$sys/fs/btrfs", 1)->@*) {
+        next if $fsid !~ m/^[0-9a-f-]{36}$/;
+        my $dir = "$sys/fs/btrfs/$fsid/devices";
+        for my $device ($entries->($dir)->@*) {
+            my $dev = $read->("$dir/$device/dev");
+            die "invalid btrfs device number\n" if $dev !~ m/^\d+:\d+$/;
+            push $users->@*, "$numbers->{$dev} is a device of btrfs file system $fsid"
+                if defined($numbers->{$dev});
+        }
+    }
+    for my $target (
+        ['nvmet', "$sys/kernel/config/nvmet/subsystems"],
+        ['target_core_mod', "$sys/kernel/config/target/core"],
+    ) {
+        my ($module, $dir) = $target->@*;
+        my $optional = !-d "$sys/module/$module";
+        for my $name ($entries->($dir, $optional)->@*) {
+            if ($module eq 'nvmet') {
+                for my $ns ($entries->("$dir/$name/namespaces")->@*) {
+                    $by_path->(
+                        $read->("$dir/$name/namespaces/$ns/device_path"),
+                        "exported by NVMe target subsystem '$name'",
+                    );
+                }
+            } elsif ($name =~ m/^(?:iblock|pscsi)_\d+$/) {
+                for my $store ($entries->("$dir/$name")->@*) {
+                    next if !-d "$dir/$name/$store";
+                    $by_path->(
+                        $read->("$dir/$name/$store/udev_path"),
+                        "exported by LIO backstore '$store'",
+                    );
+                }
+            }
+        }
+    }
+
+    for my $pid (grep { /^\d+$/ && $_ != $$ } $entries->($proc)->@*) {
+        for my $fd ($entries->("$proc/$pid/fd", 1)->@*) {
+            my $file = "$proc/$pid/fd/$fd";
+            my $target = readlink($file);
+            if (!defined($target)) {
+                next if $! == ENOENT || $! == ESRCH;
+                die "unable to read '$file' - $!\n";
+            }
+            # A process can open a device node outside /dev, including in another mount namespace.
+            next if $target !~ m|^/|;
+            my $dev = eval { block_device_number($file) };
+            if (my $err = $@) {
+                next if $! == ENOENT || $! == ESRCH;
+                die $err;
+            }
+            next if !defined($dev) || !defined($numbers->{$dev});
+            my $comm = file_read_firstline("$proc/$pid/comm") // '?';
+            push $users->@*, "$comm (PID $pid) has $numbers->{$dev} open";
+        }
+    }
+    return $users;
+}
+
 sub init_disk {
     my ($disk, $uuid) = @_;
 
diff --git a/src/PVE/Storage/LVMPlugin.pm b/src/PVE/Storage/LVMPlugin.pm
index 916f1b8f..723ed555 100644
--- a/src/PVE/Storage/LVMPlugin.pm
+++ b/src/PVE/Storage/LVMPlugin.pm
@@ -166,6 +166,13 @@ sub lvm_create_volume_group {
     my $check_instance = sub { $guard->{verify}->() if $guard && $guard->{verify} };
     my $check_safe = sub {
         $check_instance->();
+        if ($san) {
+            my $users = PVE::Diskmanage::block_device_users(
+                [basename($device)], { 'partition-maps' => 1 },
+            );
+            die "cannot use '$device' - " . join(', ', $users->@*) . "\n" if $users->@*;
+            $check_instance->();
+        }
     };
     my $refuse_data = sub {
         my (@types) = @_;
@@ -1089,7 +1096,15 @@ sub on_add_hook {
 
         # The storage API runs this hook under the cluster-wide storage configuration lock, which
         # the disk API also takes for shared devices, so two nodes cannot initialize the same LUN.
-        lvm_create_volume_group($path, $scfg->{vgname}, $scfg->{shared});
+        lvm_create_volume_group(
+            $path,
+            $scfg->{vgname},
+            $scfg->{shared},
+            {
+                reference => $path,
+                san => 1,
+            },
+        );
     }
 
     assert_discard_supported($scfg->{vgname}, $scfg->{'on-volume-remove'}, $scfg->{nodes});
diff --git a/src/test/Makefile b/src/test/Makefile
index c287ebdd..9962bdfd 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -9,9 +9,10 @@ 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_base_conflicts.pl
+test_lvm_create: run_test_lvm_create.pl run_test_lvm_base_conflicts.pl run_san_lvm_create_tests.pl
 	./run_test_lvm_create.pl
 	./run_test_lvm_base_conflicts.pl
+	./run_san_lvm_create_tests.pl
 
 test_lvm_lock_retry: run_test_lvm_lock_retry.pl
 	./run_test_lvm_lock_retry.pl
@@ -29,8 +30,9 @@ test_status_command: run_status_command_tests.pl run_status_command_contention_t
 	./run_status_command_tests.pl
 	./run_status_command_contention_tests.pl
 
-test_disklist: run_disk_tests.pl
+test_disklist: run_disk_tests.pl run_block_device_users_tests.pl
 	./run_disk_tests.pl
+	./run_block_device_users_tests.pl
 
 test_disk_api: run_disk_api_tests.pl
 	./run_disk_api_tests.pl
diff --git a/src/test/disk_api_test.pm b/src/test/disk_api_test.pm
index 893b4e04..d1fc6465 100644
--- a/src/test/disk_api_test.pm
+++ b/src/test/disk_api_test.pm
@@ -162,32 +162,6 @@ subtest 'a volume group on a map is created under the storage configuration lock
         ],
         'a map or SAN LUN is initialized under the cluster-wide lock, a local disk is not',
     );
-    @created = ();
-    $disk->mock(assert_disk_unused => sub { die "device is already in use\n" });
-    eval {
-        PVE::API2::Disks::LVM->create({
-            node => 'n1',
-            name => 'vg',
-            device => '/dev/sdc',
-        });
-    };
-    like($@, qr/already in use/, 'disk API rejects a used inventory entry');
-    is_deeply(\@created, [], 'used inventory prevents initialization');
-    $disk->mock(assert_disk_unused => sub { });
-    $lvm->mock(lvm_vgs => sub { { vg => {} } });
-    eval {
-        PVE::API2::Disks::LVM->create({
-            node => 'n1',
-            name => 'vg',
-            device => '/dev/sdc',
-        });
-    };
-    like(
-        $@,
-        qr/volume group with name 'vg' already exists/,
-        'stale inventory does not permit an existing VG',
-    );
-    is_deeply(\@created, [], 'existing VG prevents initialization');
     $disk->unmock($_)
         for qw(
         locked_disk_action verify_blockdev_path assert_disk_unused is_partition
diff --git a/src/test/run_block_device_users_tests.pl b/src/test/run_block_device_users_tests.pl
new file mode 100755
index 00000000..5a27b503
--- /dev/null
+++ b/src/test/run_block_device_users_tests.pl
@@ -0,0 +1,134 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use File::Path qw(make_path remove_tree);
+use File::Temp qw(tempdir);
+use Test::MockModule;
+use Test::More;
+
+use lib ('.', '..');
+use PVE::Diskmanage;
+
+my $root = tempdir(CLEANUP => 1);
+my $options = { sysfs => "$root/sys", procfs => "$root/proc" };
+my $disk = Test::MockModule->new('PVE::Diskmanage');
+my %numbers = ('/dev/alias' => '8:1');
+$disk->mock(
+    block_device_number => sub {
+        my ($path) = @_;
+        die "stat failed\n" if !exists($numbers{$path});
+        return $numbers{$path};
+    },
+);
+
+my sub write_file {
+    my ($path, $value) = @_;
+    my ($dir) = "$root$path" =~ m{^(.*)/[^/]+$};
+    make_path($dir);
+    open(my $fh, '>', "$root$path") or die $!;
+    print $fh $value;
+    close($fh) or die $!;
+}
+
+my sub device {
+    my ($name, $number, $parent) = @_;
+    write_file("/sys/class/block/$name/dev", "$number\n");
+    make_path("$root/sys/class/block/$name/holders");
+    if ($parent) {
+        write_file("/sys/class/block/$name/partition", "1\n");
+        symlink("../$name", "$root/sys/class/block/$parent/$name") or die $!;
+    }
+}
+
+my sub users {
+    return PVE::Diskmanage::block_device_users(['sda'], $options);
+}
+
+write_file('/proc/self/mountinfo', "1 0 0:1 / / rw - rootfs rootfs rw\n");
+write_file('/proc/swaps', "Filename\tType\tSize\tUsed\tPriority\n");
+device('sda', '8:0');
+device('sda1', '8:1', 'sda');
+is_deeply(users(), [], 'unused disk and partition');
+write_file('/proc/self/mountinfo', "1 0 8:1 / /mnt rw - ext4 /dev/alias rw\n");
+is_deeply(users(), ['sda1 mounted at /mnt'], 'mounts matched by number');
+write_file('/proc/self/mountinfo', "1 0 0:1 / / rw - rootfs rootfs rw\n");
+write_file('/proc/swaps', "Filename\tType\tSize\tUsed\tPriority\n/dev/alias partition 1 0 -2\n");
+is_deeply(users(), ['sda1 used as swap'], 'swap alias');
+write_file('/proc/swaps', "Filename\tType\tSize\tUsed\tPriority\n");
+
+device('dm-0', '253:0');
+device('dm-1', '253:1', 'dm-0');
+symlink('../../dm-0', "$root/sys/class/block/sda/holders/dm-0") or die $!;
+is_deeply(users(), ['sda held by dm-0'], 'recursive holder');
+write_file('/proc/self/mountinfo', "1 0 253:1 / /mapped rw - ext4 /dev/mapper/part rw\n");
+is_deeply(users(), ['sda held by dm-0', 'dm-1 mounted at /mapped'], 'map partition by attribute');
+write_file('/proc/self/mountinfo', "1 0 0:1 / / rw - rootfs rootfs rw\n");
+
+make_path("$root/proc/999999/fd");
+symlink('/dev/alias', "$root/proc/999999/fd/3") or die $!;
+$numbers{"$root/proc/999999/fd/3"} = '8:1';
+write_file('/proc/999999/comm', "qemu\n");
+is_deeply(
+    PVE::Diskmanage::block_device_users(
+        ['sda'], $options,
+    ),
+    ['sda held by dm-0', 'qemu (PID 999999) has sda1 open'],
+    'holders and direct users of a member partition are both listed',
+);
+unlink("$root/proc/999999/fd/3") or die $!;
+symlink('/srv/raw-disk', "$root/proc/999999/fd/3") or die $!;
+is_deeply(
+    PVE::Diskmanage::block_device_users(
+        ['sda'], $options,
+    ),
+    ['sda held by dm-0', 'qemu (PID 999999) has sda1 open'],
+    'device node outside /dev is not silently skipped',
+);
+unlink("$root/proc/999999/fd/3") or die $!;
+
+write_file('/sys/class/block/loop0/loop/backing_file', "/dev/alias\n");
+write_file('/sys/kernel/config/nvmet/subsystems/test/namespaces/1/device_path', "/dev/alias\n");
+write_file('/sys/kernel/config/target/core/iblock_0/test/udev_path', "/dev/alias\n");
+my $fsid = '00000000-0000-0000-0000-000000000001';
+write_file("/sys/fs/btrfs/$fsid/devices/sda1/dev", "8:1\n");
+is_deeply(
+    users(),
+    [
+        'sda held by dm-0',
+        'sda1 backs loop0',
+        "sda1 is a device of btrfs file system $fsid",
+        "sda1 exported by NVMe target subsystem 'test'",
+        "sda1 exported by LIO backstore 'test'",
+    ],
+    'kernel users without process descriptors',
+);
+
+unlink("$root/sys/class/block/sda1/dev") or die $!;
+eval { users() };
+like($@, qr/missing value/, 'missing partition identity fails closed');
+write_file('/sys/class/block/sda1/dev', "8:1\n");
+remove_tree("$root/sys/class/block/sda1/holders");
+eval { users() };
+like($@, qr/unable to list/, 'missing holders directory fails closed');
+make_path("$root/sys/class/block/sda1/holders");
+write_file('/proc/self/mountinfo', 'garbage');
+eval { users() };
+like($@, qr/malformed mountinfo/, 'bad mountinfo fails closed');
+write_file('/proc/self/mountinfo', "1 0 0:1 / / rw - rootfs rootfs rw\n");
+write_file('/proc/swaps', 'garbage');
+eval { users() };
+like($@, qr/malformed swaps/, 'bad swaps fails closed');
+write_file('/proc/swaps', "Filename\tType\tSize\tUsed\tPriority\n");
+$numbers{'/dev/alias'} = undef;
+delete $numbers{'/dev/alias'};
+eval { users() };
+like($@, qr/stat failed/, 'unreadable backing device fails closed');
+$numbers{'/dev/alias'} = '8:1';
+remove_tree("$root/sys/kernel/config/nvmet");
+make_path("$root/sys/module/nvmet");
+eval { users() };
+like($@, qr/unable to list.*nvmet/, 'loaded target without readable configfs fails closed');
+
+done_testing();
diff --git a/src/test/run_san_lvm_create_tests.pl b/src/test/run_san_lvm_create_tests.pl
new file mode 100755
index 00000000..200fd7cb
--- /dev/null
+++ b/src/test/run_san_lvm_create_tests.pl
@@ -0,0 +1,230 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use File::Basename qw(basename);
+use File::Path qw(make_path);
+use File::Temp qw(tempdir);
+use JSON qw(encode_json);
+use Storable qw(dclone);
+use Test::MockModule;
+use Test::More;
+
+use lib ('.', '..');
+use PVE::API2::Disks::LVM;
+use PVE::API2::Storage::Config;
+
+my $disk = Test::MockModule->new('PVE::Diskmanage');
+my $storage = Test::MockModule->new('PVE::Storage');
+my $lvm = Test::MockModule->new('PVE::Storage::LVMPlugin');
+my $rpc = Test::MockModule->new('PVE::RPCEnvironment');
+my $inotify = Test::MockModule->new('PVE::INotify');
+my $tools = Test::MockModule->new('PVE::Tools');
+my $glob = \&PVE::Tools::dir_glob_foreach;
+my $users = \&PVE::Diskmanage::block_device_users;
+my ($root, $resolved, $worker, $cfg, $local, $pv, $during_probe, $inventory_used);
+my (@locks, @writes);
+my $uuid = 'aaaaaa-bbbb-cccc-dddd-eeee-ffff-gggggg';
+my $canonical = '/dev/mapper/lun';
+my sub put {
+    my ($path, $value) = @_;
+    my ($dir) = "$root/$path" =~ m{^(.*)/[^/]+$};
+    make_path($dir);
+    open(my $fh, '>', "$root/$path") or die $!;
+    print $fh "$value\n";
+    close($fh) or die $!;
+}
+my sub locked {
+    my ($name, $code) = @_;
+    push @locks, $name;
+    my $res = eval { $code->() };
+    my $err = $@;
+    pop @locks;
+    die $err if $err;
+    return $res;
+}
+my sub written {
+    my ($tool, $path) = @_;
+    push @writes, [$tool, $path];
+}
+{
+
+    package SanLVMTestRPC;
+    sub get_user { 'root@pam' }
+    sub check { 1 }
+    sub fork_worker { $worker = $_[4]; 'test-upid' }
+}
+$rpc->mock(get => sub { bless {}, 'SanLVMTestRPC' });
+$inotify->mock(nodename => sub { 'n1' });
+$disk->mock(
+    verify_blockdev_path => sub { $_[0] =~ m{^/dev/mapper/} ? $resolved : $_[0] },
+    is_san_device => sub { !$local },
+    is_multipath_device => sub { !$local && $_[0] =~ /dm-/ },
+    is_partition => sub { 0 },
+    block_device_users => sub {
+        $users->($_[0], { %{ $_[1] // {} }, sysfs => "$root/sys", procfs => "$root/proc" });
+    },
+    block_device_number => sub {
+        my $path = readlink($_[0]) // $_[0];
+        return PVE::Tools::file_read_firstline(
+            "$root/sys/class/block/" . basename($path) . '/dev');
+    },
+    is_mounted => sub { undef },
+    has_holder => sub { undef },
+    assert_disk_unused => sub { die "device '$_[0]' is already in use\n" if $inventory_used },
+    locked_disk_action => sub { locked('disk', $_[0]) },
+    udevadm_trigger => sub { },
+);
+$storage->mock(
+    lock_storage_config => sub {
+        die "no quorum\n" if $local;
+        ok(
+            !@locks || join('>', @locks) eq 'disk',
+            'storage lock never precedes a nested disk lock',
+        );
+        locked('storage', $_[0]);
+    },
+    config =>
+        sub { is($locks[-1], 'storage', 'configuration read under storage lock'); dclone($cfg) },
+    write_config => sub { },
+    path => sub { $canonical },
+    activate_storage => sub { },
+    activate_volumes => sub { },
+);
+$tools->mock(
+    dir_glob_foreach => sub {
+        my $path = $_[0];
+        $path =~ s{^/sys}{$root/sys};
+        $glob->($path, $_[1], $_[2]);
+    },
+);
+$lvm->mock(
+    file_read_firstline => sub {
+        my $path = $_[0];
+        $path =~ s{^/sys}{$root/sys};
+        PVE::Tools::file_read_firstline($path);
+    },
+    abs_path => sub { $_[0] },
+    clear_first_sector => sub { written('clear', $_[0]) },
+    lvm_vgs => sub { $pv eq 'same' ? { vg => {} } : {} },
+    assert_discard_supported => sub { },
+    run_command => sub {
+        my ($cmd, %opt) = @_;
+        if ($cmd->[0] eq 'wipefs') {
+            $opt{outfunc}
+                ->(encode_json({ signatures => $pv eq 'blank' ? [] : [{ type => 'LVM2_member' }] })
+                );
+        } elsif ($cmd->[0] eq '/usr/bin/file') {
+            $opt{outfunc}->("$cmd->[-1]: " . ($pv eq 'blank' ? 'data' : 'LVM2 PV'));
+            $during_probe->() if $during_probe;
+        } elsif ($cmd->[0] eq '/sbin/pvs') {
+            $opt{outfunc}->("$resolved:8192:"
+                . ($pv eq 'same' ? 'vg' : '')
+                . ":$uuid:"
+                . ($pv eq 'same' ? '1' : '0'));
+        } else {
+            written($cmd->[0], $cmd->[-1]);
+            $pv = 'orphan' if $cmd->[0] eq '/sbin/pvcreate';
+        }
+    },
+);
+my sub reset_case {
+    $root = tempdir(CLEANUP => 1);
+    for my $name ('dm-900', 'vdz') {
+        put("sys/class/block/$name/dev", $name eq 'vdz' ? '252:0' : '253:900');
+        put("sys/class/block/$name/size", '8192');
+        make_path("$root/sys/class/block/$name/holders");
+    }
+    put('sys/class/block/dm-900/dm/uuid', 'mpath-test');
+    put('sys/class/block/dm-900/slaves/sda/device/state', 'running');
+    put('proc/self/mountinfo', '1 0 0:1 / / rw - rootfs rootfs rw');
+    put('proc/swaps', "Filename\tType\tSize\tUsed\tPriority");
+    ($resolved, $local, $pv) = ('/dev/dm-900', 0, 'blank');
+    ($during_probe, $worker) = (undef, undef);
+    $cfg =
+        { ids => { base => { type => 'iscsi', target => 'iqn.test', content => { none => 1 } } } };
+    @writes = ();
+    @locks = ();
+    $inventory_used = 0;
+}
+my sub queue {
+    PVE::API2::Disks::LVM->create({ node => 'n1', name => 'vg', device => $_[0] // $canonical });
+}
+my sub run_route {
+    my ($route) = @_;
+    eval {
+        if ($route eq 'disk') {
+            queue();
+            $worker->();
+        } else {
+            PVE::API2::Storage::Config->create({
+                type => 'lvm',
+                storage => 'new',
+                vgname => 'vg',
+                base => 'base:0.0.1.lun',
+            });
+        }
+    };
+    return $@;
+}
+for my $route ('disk', 'base') {
+    reset_case();
+    make_path("$root/proc/999999/fd");
+    symlink('/dev/dm-900', "$root/proc/999999/fd/3") or die $!;
+    put('proc/999999/comm', 'qemu');
+    like(
+        run_route($route),
+        qr/cannot use.*qemu .* has dm-900 open/,
+        "$route raw map opener refused",
+    );
+    is_deeply(\@writes, [], 'raw user receives no destructive command');
+    reset_case();
+    unlink("$root/proc/swaps") or die $!;
+    like(run_route($route), qr{can't open '.*/proc/swaps'}, "$route inconclusive users refuse");
+    is_deeply(\@writes, [], 'inconclusive users receive no destructive command');
+
+    for my $kind ('blank', 'orphan', 'same') {
+        reset_case();
+        $pv = $kind;
+        if ($kind eq 'same' && $route eq 'disk') {
+            $inventory_used = 1;
+            like(
+                run_route($route),
+                qr/device .* is already in use/,
+                'disk API rejects the used inventory entry for an existing VG',
+            );
+            is_deeply(\@writes, [], 'used disk inventory prevents every write');
+            $inventory_used = 0;
+            like(
+                run_route($route),
+                qr/volume group with name 'vg' already exists/,
+                'disk API rejects an existing VG even with a stale unused hint',
+            );
+            is_deeply(\@writes, [], 'existing VG prevents every disk API write');
+        } else {
+            is(run_route($route), '', "$route $kind succeeds");
+            is(
+                scalar(@writes),
+                $kind eq 'same' ? 0 : 3,
+                'only the base-route same-VG retry succeeds without writes',
+            );
+        }
+    }
+}
+reset_case();
+$local = 1;
+$resolved = '/dev/vdz';
+queue('/dev/vdz');
+eval { $worker->() };
+is($@, '', 'local initialization works without quorum');
+is(scalar(@writes), 3, 'local behavior keeps its writes');
+reset_case();
+$local = 1;
+queue('/dev/vdz');
+$local = 0;
+eval { $worker->() };
+like($@, qr/cannot use.*device topology changed/, 'queued local-to-SAN transition refuses');
+is_deeply(\@writes, [], 'transition does not bypass storage lock');
+
+done_testing();
-- 
2.47.3




  parent reply	other threads:[~2026-11-06  0:54 UTC|newest]

Thread overview: 77+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-05 23:30 [PATCH v3 proxmox,proxmox-perl-rs,cluster,storage,docs,manager 00/76] SAN: cluster-wide multipath, SAN views and a guided storage setup Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 proxmox 01/76] add proxmox-multipath crate Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 proxmox-perl-rs 02/76] pve-rs: add PVE::RS::Multipath bindings for multipathd status parsing Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 cluster 03/76] cfs: observe the multipath configuration files Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 04/76] storage: distinguish status-only activation in plugins Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 05/76] lvm: find a shared volume group created by another node Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 06/76] storage: bound LVM status commands Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 07/76] iscsi: rate-limit session rescans per target Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 08/76] iscsi: list the LUNs of each session, not of its SCSI host Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 09/76] lvm: refuse to create a volume group on a device that holds data Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 10/76] lvm: refuse a base volume of a target that serves disk images Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 11/76] iscsi: refuse virtual devices other than multipath maps as volumes Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 12/76] diskmanage: collect disk transport type from lsblk Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 13/76] diskmanage: add helper to list multipath devices Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 14/76] diskmanage: qualify NVMe over fabrics transport Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 15/76] disks: list: add include-remote parameter Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 16/76] diskmanage: include iSCSI session devices in disk enumeration Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 17/76] diskmanage: link multipath member disks to their map device Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 18/76] iscsi: factor out session device map from device list Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 19/76] api: scan: add a SAN LUN inventory Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 20/76] pvesm: expose the " Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 21/76] disks: lvm: allow volume groups on SAN LUNs and multipath maps Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 22/76] multipath: add a library around multipathd Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 23/76] diskmanage: collect native NVMe multipath path state Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 24/76] api: scan: san-luns: report " Thomas Lamprecht
2026-11-05 23:31 ` Thomas Lamprecht [this message]
2026-11-05 23:31 ` [PATCH v3 storage 26/76] api: add node-level iSCSI status and maintenance Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 27/76] api: iscsi: add session logout with last-path protection Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 28/76] multipath: add a cluster-wide configuration Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 29/76] iscsi: add an option to manage storage LUNs with multipath Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 30/76] lvm: activate a missing base volume before using the volume group Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 31/76] multipath: apply the configuration on every node Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 32/76] iscsi: share the initiator identity reader Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 33/76] multipath: publish per-node status Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 34/76] api: add endpoints for the cluster-wide multipath configuration Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 35/76] api: multipath: add the cluster-wide health status Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 36/76] multipath: collect identities of non-iSCSI SCSI paths Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 37/76] api: scan: san-luns: report the multipath state of each LUN Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 38/76] api: scan: flag duplicate physical volumes on distinct LUNs Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 39/76] api: disks: recheck local users and retain wipe device references Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 40/76] iscsi: name plain LUNs after their identity link Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 41/76] api: disks: guard SAN wipes with stable identities and storage checks Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 42/76] api: scan: refresh SAN usage with bounded probes Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 docs 43/76] storage: lvm: note the rescan after extending a shared volume group Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 docs 44/76] storage: document the cluster-wide multipath management Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 45/76] api: ceph: osd: keep iSCSI LUNs out of OSD creation Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 46/76] pvestatd: track pending storage status commands across re-exec Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 47/76] ui: storage: factor out the scan picker error mask Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 48/76] ui: storage: nfs: show scan errors and empty results Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 49/76] ui: storage: allow changing the node for NFS/CIFS scans Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 50/76] ui: storage: add guided remote storage wizard with NFS support Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 51/76] ui: storage wizard: add SMB/CIFS support Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 52/76] ui: storage wizard: preserve settings after partial setup Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 53/76] ui: storage wizard: add Fibre Channel shared LVM setup Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 54/76] ui: storage wizard: reconcile unanswered storage creation Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 55/76] ui: storage wizard: add ZFS over iSCSI support Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 56/76] ui: dc: storage: add remote storage wizard entry to the add menu Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 57/76] ui: node: add SAN LUNs panel Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 58/76] ui: san luns: show multipath path state Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 59/76] api: nodes: expose iSCSI status and maintenance Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 60/76] pvenode: add iscsi commands Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 61/76] ui: node: san: add iSCSI target and session management Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 62/76] pvestatd: apply the multipath configuration and report the node status Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 63/76] api: cluster: add the multipath endpoints Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 64/76] ui: dc: add a cluster-wide multipath view Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 65/76] ui: storage: iscsi: add the multipath option Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 66/76] ui: san luns: show multipath state and manage LUN entries Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 67/76] ui: storage wizard: add iSCSI support Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 68/76] ui: storage wizard: iscsi: show cluster initiator names Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 69/76] ui: storage wizard: iscsi: enable multipath for shared LVM Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 70/76] ui: node: san: wipe a LUN that no storage uses Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 71/76] ui: storage wizard: iscsi: reuse existing target connections Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 72/76] ui: storage wizard: fibre channel: enable multipath for a LUN Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 73/76] ui: storage wizard: let root@pam wipe a LUN with data Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 74/76] ui: storage wizard: do not offer LUNs in use for a new volume group Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 75/76] ui: san: show the local users of a LUN in its usage tooltip Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 76/76] ui: san: check the other nodes for users before wiping a LUN Thomas Lamprecht

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261105233336.2178064-26-t.lamprecht@proxmox.com \
    --to=t.lamprecht@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal