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
next prev 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