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 CA0301FF0ED for ; Fri, 31 Jul 2026 12:23:35 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id EFD0E216B0; Fri, 31 Jul 2026 12:22:06 +0200 (CEST) From: Dietmar Maurer To: pve-devel@lists.proxmox.com Subject: [RFC pve-storage 08/27] api: scan: add san-luns method listing SAN LUN candidates Date: Fri, 31 Jul 2026 12:21:37 +0200 Message-ID: <20260731102156.3947857-9-dietmar@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260731102156.3947857-1-dietmar@proxmox.com> References: <20260731102156.3947857-1-dietmar@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 2 AWL -0.289 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_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 1.274 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record Message-ID-Hash: 2BTUT4CPR5FUFNMMBAOKMTZS2REYCCBT X-Message-ID-Hash: 2BTUT4CPR5FUFNMMBAOKMTZS2REYCCBT X-MailFrom: dietmar@zilli.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: The storage wizard needs to offer the block devices of a SAN as candidates for a shared LVM volume group. The node disk list alone is not sufficient for that: aggregated multipath devices are missing and mapping a device to its volume group and to a storage definition that already uses it requires combining several calls. Return one entry per whole disk or multipath mapped device with its transport type and usage: unused devices are candidates for a new volume group, devices holding an LVM physical volume report the volume group name and, if the volume group is already referenced by a storage definition, the storage ID, so a UI can filter those out. LUNs belonging to the target of a configured iSCSI storage report that storage as well, so a UI can warn before placing a volume group on a LUN that already serves as raw VM disk storage. Member paths of a multipath device are folded into the mapped device. Also expose the method as 'pvesm scan san-luns'. Signed-off-by: Dietmar Maurer --- src/PVE/API2/Storage/Scan.pm | 175 +++++++++++++++++++++++++++++++++++ src/PVE/CLI/pvesm.pm | 8 ++ 2 files changed, 183 insertions(+) diff --git a/src/PVE/API2/Storage/Scan.pm b/src/PVE/API2/Storage/Scan.pm index a58bdd8..9b944d5 100644 --- a/src/PVE/API2/Storage/Scan.pm +++ b/src/PVE/API2/Storage/Scan.pm @@ -5,10 +5,12 @@ use warnings; # NOTE: This API endpoints are mounted by pve-manager's API2::Node module and pvesm CLI +use PVE::Diskmanage; use PVE::JSONSchema qw(get_standard_option); use PVE::RESTHandler; use PVE::SafeSyslog; +use PVE::Storage::ISCSIPlugin; use PVE::Storage::LVMPlugin; use PVE::Storage::LvmThinPlugin; use PVE::Storage::PBSPlugin; @@ -49,6 +51,7 @@ __PACKAGE__->register_method({ { method => 'lvm' }, { method => 'nfs' }, { method => 'pbs' }, + { method => 'san-luns' }, { method => 'zfs' }, ]; @@ -373,6 +376,178 @@ __PACKAGE__->register_method({ }, }); +__PACKAGE__->register_method({ + name => 'sanlunscan', + path => 'san-luns', + method => 'GET', + description => "List block devices (disks and multipath devices) together with their" + . " usage, for example as candidates for a SAN backed LVM volume group.", + protected => 1, + proxyto => "node", + permissions => { + check => ['perm', '/storage', ['Datastore.Allocate']], + }, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { + devpath => { + description => "The device path.", + type => 'string', + }, + size => { + description => "The device size in bytes.", + type => 'integer', + optional => 1, + }, + transport => { + description => "The bus type the device is attached with (for example" + . " sata, sas, usb, fc, iscsi, nvme, nvme-fc, nvme-tcp).", + type => 'string', + optional => 1, + }, + vendor => { type => 'string', optional => 1 }, + model => { type => 'string', optional => 1 }, + serial => { type => 'string', optional => 1 }, + wwn => { type => 'string', optional => 1 }, + paths => { + description => "The number of paths of a multipath mapped device.", + type => 'integer', + optional => 1, + }, + usage => { + description => "How the device is currently used. 'lvm' means it is an LVM" + . " physical volume, see the 'vgname' property.", + type => 'string', + enum => ['unused', 'lvm', 'other'], + }, + vgname => { + description => "The name of the LVM volume group the device belongs to.", + type => 'string', + optional => 1, + }, + 'used-by-storage' => { + description => "The ID of an existing storage definition that uses this" + . " device, either through the LVM volume group on it or because it" + . " is a LUN of the storage's iSCSI target.", + type => 'string', + optional => 1, + }, + detail => { + description => + "Details about other usage, for example the file system type.", + type => 'string', + optional => 1, + }, + }, + }, + }, + code => sub { + my ($param) = @_; + + my $disks = PVE::Diskmanage::get_disks(undef, 1, 0); + my $multipath = PVE::Diskmanage::get_multipath_disks(); + + # map PV device to VG name, PVs may sit on a partition of a listed device + my $vgs = PVE::Storage::LVMPlugin::lvm_vgs(1); + my $pv2vg = {}; + for my $vgname (sort keys %$vgs) { + for my $pv (@{ $vgs->{$vgname}->{pvs} // [] }) { + $pv2vg->{ $pv->{name} } = $vgname; + } + } + my $find_vg = sub { + my ($devpath) = @_; + return $pv2vg->{$devpath} if defined($pv2vg->{$devpath}); + for my $pv (sort keys %$pv2vg) { + return $pv2vg->{$pv} if $pv =~ m/^\Q$devpath\Ep?\d+$/; + } + return undef; + }; + + my $cfg = PVE::Storage::config(); + my $vg_used_by = {}; + my $iscsi_target_used_by = {}; + for my $sid (sort keys %{ $cfg->{ids} }) { + my $scfg = $cfg->{ids}->{$sid}; + if (defined(my $vgname = $scfg->{vgname})) { + $vg_used_by->{$vgname} //= $sid; + } + if ($scfg->{type} eq 'iscsi' && defined(my $target = $scfg->{target})) { + $iscsi_target_used_by->{$target} //= $sid; + } + } + + # map kernel device name to the storage using its iSCSI target + my $iscsi_used_by = {}; + if (scalar(keys %$iscsi_target_used_by)) { + my $device_map = PVE::Storage::ISCSIPlugin::iscsi_session_device_map(); + for my $bdev (sort keys %$device_map) { + my $sid = $iscsi_target_used_by->{ $device_map->{$bdev}->{target} } // next; + $iscsi_used_by->{$bdev} = $sid; + } + } + + my $res = []; + my $add_entry = sub { + my ($disk) = @_; + + my $entry = { + devpath => $disk->{devpath}, + size => $disk->{size}, + }; + for my $key (qw(transport vendor model serial wwn paths)) { + $entry->{$key} = $disk->{$key} if defined($disk->{$key}); + } + + if (!defined($disk->{used})) { + $entry->{usage} = 'unused'; + } elsif ($disk->{used} eq 'LVM') { + $entry->{usage} = 'lvm'; + if (defined(my $vgname = $find_vg->($disk->{devpath}))) { + $entry->{vgname} = $vgname; + if (defined(my $sid = $vg_used_by->{$vgname})) { + $entry->{'used-by-storage'} = $sid; + } + } + } else { + $entry->{usage} = 'other'; + $entry->{detail} = $disk->{used}; + } + + if (!defined($entry->{'used-by-storage'})) { + my @members = + map { $_ =~ s|^/dev/||r } ($disk->{slaves} // [$disk->{devpath}])->@*; + for my $member (@members) { + if (defined(my $sid = $iscsi_used_by->{$member})) { + $entry->{'used-by-storage'} = $sid; + last; + } + } + } + + push @$res, $entry; + }; + + for my $dev (sort keys %$disks) { + my $disk = $disks->{$dev}; + # member paths are represented by their multipath device + next if defined($disk->{multipath}); + $add_entry->($disk); + } + $add_entry->($multipath->{$_}) for sort keys %$multipath; + + return $res; + }, +}); + __PACKAGE__->register_method({ name => 'zfsscan', path => 'zfs', diff --git a/src/PVE/CLI/pvesm.pm b/src/PVE/CLI/pvesm.pm index 3edf31a..7348e8e 100755 --- a/src/PVE/CLI/pvesm.pm +++ b/src/PVE/CLI/pvesm.pm @@ -741,6 +741,14 @@ our $cmddef = { $print_api_result, $PVE::RESTHandler::standard_output_options, ], + 'san-luns' => [ + "PVE::API2::Storage::Scan", + 'sanlunscan', + [], + { node => $nodename }, + $print_api_result, + $PVE::RESTHandler::standard_output_options, + ], zfs => [ "PVE::API2::Storage::Scan", 'zfsscan', -- 2.47.3