From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id AD0131FF0ED for ; Fri, 31 Jul 2026 12:23:51 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id AD60021702; Fri, 31 Jul 2026 12:22:07 +0200 (CEST) From: Dietmar Maurer To: pve-devel@lists.proxmox.com Subject: [RFC pve-storage 12/27] api: scan: san-luns: report path state Date: Fri, 31 Jul 2026 12:21:41 +0200 Message-ID: <20260731102156.3947857-13-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.257 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: KNOXYAR2B56Y7H62NKHW5NXCOCPF3TEW X-Message-ID-Hash: KNOXYAR2B56Y7H62NKHW5NXCOCPF3TEW 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 path count from the sysfs slave list cannot detect a degraded multipath map: a faulty path stays in the device mapper table until the transport removes the device. Enrich aggregated multipath entries with the active path count, the fault counter and the per-path states from multipathd, so clients can show path health per LUN. NVMe namespaces handled by native NVMe multipath get the equivalent controller and ANA states from sysfs. The queries are best-effort, on failure the entries simply lack the new optional fields. Signed-off-by: Dietmar Maurer --- src/PVE/API2/Storage/Scan.pm | 96 +++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/src/PVE/API2/Storage/Scan.pm b/src/PVE/API2/Storage/Scan.pm index 9b944d5..b8e5bbe 100644 --- a/src/PVE/API2/Storage/Scan.pm +++ b/src/PVE/API2/Storage/Scan.pm @@ -381,7 +381,9 @@ __PACKAGE__->register_method({ 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.", + . " usage, for example as candidates for a SAN backed LVM volume group. For multipath" + . " mapped devices and NVMe namespaces handled by native NVMe multipath, the state" + . " of the member paths is included.", protected => 1, proxyto => "node", permissions => { @@ -418,10 +420,76 @@ __PACKAGE__->register_method({ serial => { type => 'string', optional => 1 }, wwn => { type => 'string', optional => 1 }, paths => { - description => "The number of paths of a multipath mapped device.", + description => "The number of paths of a multipath mapped device or of" + . " an NVMe namespace attached through multiple controllers.", type => 'integer', optional => 1, }, + 'active-paths' => { + description => "The number of paths that are usable, that is, neither" + . " reported as failed by multipathd nor on a dead controller or" + . " inaccessible through ANA.", + type => 'integer', + optional => 1, + }, + 'path-faults' => { + description => "The number of path failures of a multipath mapped" + . " device since its creation, as reported by multipathd.", + type => 'integer', + optional => 1, + }, + 'path-list' => { + description => "The member paths of the device with their state.", + type => 'array', + optional => 1, + items => { + type => 'object', + properties => { + device => { + description => "The path block device, or the NVMe" + . " controller device the path uses.", + type => 'string', + }, + state => { + description => "The device mapper state of the path (for" + . " example active or failed) or the state of the NVMe" + . " controller (for example live or connecting).", + type => 'string', + }, + 'checker-state' => { + description => "The state reported by the multipathd path" + . " checker, for example ready, ghost or faulty.", + type => 'string', + optional => 1, + }, + 'ana-state' => { + description => "The Asymmetric Namespace Access state of" + . " the namespace on this path, for example optimized" + . " or inaccessible.", + type => 'string', + optional => 1, + }, + 'host-wwpn' => { + description => "The WWPN of the host (initiator) port of" + . " Fibre Channel attached paths.", + type => 'string', + optional => 1, + }, + 'target-wwpn' => { + description => "The WWPN of the target port of Fibre" + . " Channel attached paths.", + type => 'string', + optional => 1, + }, + address => { + description => "The address of the NVMe controller the" + . " path uses.", + type => 'string', + optional => 1, + }, + }, + }, + }, usage => { description => "How the device is currently used. 'lvm' means it is an LVM" . " physical volume, see the 'vgname' property.", @@ -455,6 +523,15 @@ __PACKAGE__->register_method({ my $disks = PVE::Diskmanage::get_disks(undef, 1, 0); my $multipath = PVE::Diskmanage::get_multipath_disks(); + # path state is auxiliary information, a multipathd failure must not break the scan + my $mp_status = {}; + if (scalar(keys %$multipath)) { + $mp_status = eval { PVE::Diskmanage::get_multipath_status() } // {}; + warn $@ if $@; + } + my $nvme_status = eval { PVE::Diskmanage::get_nvme_path_status() } // {}; + warn $@ if $@; + # 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 = {}; @@ -507,6 +584,21 @@ __PACKAGE__->register_method({ $entry->{$key} = $disk->{$key} if defined($disk->{$key}); } + # aggregated multipath entries carry their member list and their WWID as wwn + if (defined($disk->{slaves})) { + if (my $status = $mp_status->{ $disk->{wwn} // '' }) { + $entry->{'active-paths'} = $status->{active}; + $entry->{'path-faults'} = $status->{faults}; + $entry->{'path-list'} = $status->{paths}; + } + } elsif ($disk->{devpath} =~ m|^/dev/(nvme\d+n\d+)$|) { + if (my $status = $nvme_status->{$1}) { + $entry->{paths} = scalar($status->{paths}->@*); + $entry->{'active-paths'} = $status->{active}; + $entry->{'path-list'} = $status->{paths}; + } + } + if (!defined($disk->{used})) { $entry->{usage} = 'unused'; } elsif ($disk->{used} eq 'LVM') { -- 2.47.3