From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 5C1941FF14C for ; Fri, 26 Jun 2026 14:11:06 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 920DFF52D; Fri, 26 Jun 2026 14:10:52 +0200 (CEST) From: Thomas Lamprecht To: pve-devel@lists.proxmox.com Subject: [PATCH storage 02/13] api: disks: add read-only multipath status endpoint Date: Fri, 26 Jun 2026 14:07:32 +0200 Message-ID: <20260626121000.2095591-3-t.lamprecht@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260626121000.2095591-1-t.lamprecht@proxmox.com> References: <20260626121000.2095591-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: 1782475801267 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.145 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment 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 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: 35WCOJXAEQG6AY2WA4A3KTL23FZMMEMA X-Message-ID-Hash: 35WCOJXAEQG6AY2WA4A3KTL23FZMMEMA 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: Expose multipath map state under /nodes/{node}/disks/multipath so the web UI and operators can see map health, per-path transport and which LVM volume group consumes a map, rather than parsing 'multipath -ll' by hand. Signed-off-by: Thomas Lamprecht --- src/PVE/API2/Disks.pm | 7 ++ src/PVE/API2/Disks/Makefile | 1 + src/PVE/API2/Disks/Multipath.pm | 206 ++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 src/PVE/API2/Disks/Multipath.pm diff --git a/src/PVE/API2/Disks.pm b/src/PVE/API2/Disks.pm index e707a9e..984d890 100644 --- a/src/PVE/API2/Disks.pm +++ b/src/PVE/API2/Disks.pm @@ -14,6 +14,7 @@ use PVE::Tools qw(run_command); use PVE::API2::Disks::Directory; use PVE::API2::Disks::LVM; use PVE::API2::Disks::LVMThin; +use PVE::API2::Disks::Multipath; use PVE::API2::Disks::ZFS; use PVE::RESTHandler; @@ -34,6 +35,11 @@ __PACKAGE__->register_method({ path => 'directory', }); +__PACKAGE__->register_method({ + subclass => "PVE::API2::Disks::Multipath", + path => 'multipath', +}); + __PACKAGE__->register_method({ subclass => "PVE::API2::Disks::ZFS", path => 'zfs', @@ -70,6 +76,7 @@ __PACKAGE__->register_method({ { name => 'lvm' }, { name => 'lvmthin' }, { name => 'directory' }, + { name => 'multipath' }, { name => 'wipedisk' }, { name => 'zfs' }, ]; diff --git a/src/PVE/API2/Disks/Makefile b/src/PVE/API2/Disks/Makefile index 9152aed..f6f8449 100644 --- a/src/PVE/API2/Disks/Makefile +++ b/src/PVE/API2/Disks/Makefile @@ -1,6 +1,7 @@ SOURCES= LVM.pm\ LVMThin.pm\ + Multipath.pm\ ZFS.pm\ Directory.pm diff --git a/src/PVE/API2/Disks/Multipath.pm b/src/PVE/API2/Disks/Multipath.pm new file mode 100644 index 0000000..5cf2d17 --- /dev/null +++ b/src/PVE/API2/Disks/Multipath.pm @@ -0,0 +1,206 @@ +package PVE::API2::Disks::Multipath; + +use strict; +use warnings; + +use PVE::JSONSchema qw(get_standard_option); +use PVE::Multipath; +use PVE::Storage::LVMPlugin; + +use PVE::RESTHandler; + +use base qw(PVE::RESTHandler); + +my $path_groups_schema = { + type => 'array', + description => 'The path groups of the map, in priority order.', + items => { + type => 'object', + additionalProperties => 1, + properties => { + group => { type => 'integer' }, + 'dm-state' => { type => 'string', optional => 1 }, + priority => { type => 'integer' }, + paths => { + type => 'array', + items => { + type => 'object', + additionalProperties => 1, + properties => { + dev => { + type => 'string', + description => 'The underlying block device of the path.', + }, + 'dm-state' => { + type => 'string', + description => "Path state as seen by device-mapper " + . "('active' or 'failed').", + optional => 1, + }, + 'dev-state' => { + type => 'string', + description => "Path state as seen by the kernel block layer.", + optional => 1, + }, + 'check-state' => { + type => 'string', + description => 'Result of the path checker.', + optional => 1, + }, + priority => { type => 'integer', optional => 1 }, + transport => { + type => 'string', + description => 'Transport of this path (iscsi, fc, sas).', + optional => 1, + }, + }, + }, + }, + }, + }, +}; + +# Annotates each map with the LVM volume group sitting on it, if any; the map's PV shows up under +# its /dev/mapper/ path in the VG -> PV listing. +my sub annotate_lvm_usage { + my ($maps) = @_; + + return if !scalar(@$maps); + + my $pv_to_vg = {}; + eval { + my $vgs = PVE::Storage::LVMPlugin::lvm_vgs(1); + for my $vgname (keys %$vgs) { + for my $pv ($vgs->{$vgname}->{pvs}->@*) { + $pv_to_vg->{ $pv->{name} } = $vgname; + } + } + }; + warn $@ if $@; + + for my $map (@$maps) { + next if !defined($map->{name}); + my $vg = $pv_to_vg->{"/dev/mapper/$map->{name}"}; + $map->{'used-by'} = "LVM VG '$vg'" if defined($vg); + } +} + +__PACKAGE__->register_method({ + name => 'index', + path => '', + method => 'GET', + proxyto => 'node', + protected => 1, + permissions => { + check => ['perm', '/', ['Sys.Audit']], + }, + description => "List and report the health of device-mapper multipath maps.", + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { + type => 'object', + properties => { + supported => { + type => 'boolean', + description => "Whether multipath-tools is installed on the node.", + }, + running => { + type => 'boolean', + description => "Whether the multipathd daemon is reachable.", + }, + maps => { + type => 'array', + items => { + type => 'object', + additionalProperties => 1, + properties => { + wwid => { + type => 'string', + description => 'The WWID, the stable identity of the LUN.', + optional => 1, + }, + name => { + type => 'string', + description => 'The (node-local) multipath map name.', + optional => 1, + }, + path => { + type => 'string', + description => 'Stable WWID-based device path of the map.', + optional => 1, + }, + sysfs => { + type => 'string', + description => "The 'dm-N' kernel device name.", + optional => 1, + }, + size => { + type => 'integer', + description => 'Size of the map in bytes.', + optional => 1, + }, + health => { + type => 'string', + description => "Aggregated map health: 'optimal' (all paths " + . "active), 'degraded' (some paths failed) or 'failed' " + . "(no active path).", + enum => ['optimal', 'degraded', 'failed'], + }, + 'dm-state' => { type => 'string', optional => 1 }, + 'paths-total' => { + type => 'integer', + description => 'Total number of paths.', + }, + 'paths-active' => { + type => 'integer', + description => 'Number of currently active paths.', + }, + transport => { + type => 'string', + description => + "Transport shared by all of the map's paths, if uniform.", + optional => 1, + }, + used => { + type => 'boolean', + description => 'Whether something sits on the map, such as an LVM' + . ' physical volume.', + optional => 1, + }, + 'used-by' => { + type => 'string', + description => 'What consumes the map, if known, such as an LVM' + . ' volume group.', + optional => 1, + }, + 'path-groups' => $path_groups_schema, + }, + }, + }, + }, + }, + code => sub { + my ($param) = @_; + + my $supported = PVE::Multipath::is_supported(); + my $running = $supported ? PVE::Multipath::is_running() : 0; + + my $maps = []; + if ($running) { + $maps = PVE::Multipath::get_maps(); + annotate_lvm_usage($maps); + } + + return { + supported => $supported ? 1 : 0, + running => $running ? 1 : 0, + maps => $maps, + }; + }, +}); + +1; -- 2.47.3