From: Thomas Lamprecht <t.lamprecht@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH storage 03/13] api: multipath: add cluster-wide configuration endpoints
Date: Fri, 26 Jun 2026 14:07:33 +0200 [thread overview]
Message-ID: <20260626121000.2095591-4-t.lamprecht@proxmox.com> (raw)
In-Reply-To: <20260626121000.2095591-1-t.lamprecht@proxmox.com>
Add a CRUD API over the cluster-wide multipath.cfg, mounted by
pve-manager at /cluster/multipath: read the effective configuration,
manage the WWID allow-list, set the global defaults and the per-LUN
knobs and aliases, and replace the verbatim override sections.
Updates take the settable knobs straight from the section schema, with
the schema 'default' stripped so omitting a knob leaves it unchanged
rather than resetting it; 'delete' is guarded to the settable
properties so it cannot strip a section's type and corrupt the file.
Applying the result on each node is handled separately by pvestatd via
the generator.
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---
src/PVE/API2/Makefile | 1 +
src/PVE/API2/Multipath.pm | 422 ++++++++++++++++++++++++++++++++++++++
2 files changed, 423 insertions(+)
create mode 100644 src/PVE/API2/Multipath.pm
diff --git a/src/PVE/API2/Makefile b/src/PVE/API2/Makefile
index fe316c5..002a3bd 100644
--- a/src/PVE/API2/Makefile
+++ b/src/PVE/API2/Makefile
@@ -3,5 +3,6 @@
.PHONY: install
install:
install -D -m 0644 Disks.pm ${DESTDIR}${PERLDIR}/PVE/API2/Disks.pm
+ install -D -m 0644 Multipath.pm ${DESTDIR}${PERLDIR}/PVE/API2/Multipath.pm
make -C Storage install
make -C Disks install
diff --git a/src/PVE/API2/Multipath.pm b/src/PVE/API2/Multipath.pm
new file mode 100644
index 0000000..6a165d5
--- /dev/null
+++ b/src/PVE/API2/Multipath.pm
@@ -0,0 +1,422 @@
+package PVE::API2::Multipath;
+
+use strict;
+use warnings;
+
+use PVE::Exception qw(raise_param_exc);
+use PVE::Storage;
+use PVE::Tools qw(extract_param);
+
+use PVE::Multipath::Config;
+use PVE::Multipath::ClusterConfig;
+
+use PVE::RESTHandler;
+
+use base qw(PVE::RESTHandler);
+
+# WWID as exported by /dev/disk/by-id/dm-uuid-mpath-<wwid>; keep validation lenient, multipathd is
+# the authority on what is valid.
+my $WWID_RE = qr/^[a-zA-Z0-9._:-]+$/;
+
+# Map each multipath WWID to the storage that consumes it, read from the cluster-wide storage config
+# so the answer is identical on every node.
+my sub multipath_consumers {
+ my $consumers = {};
+
+ my $cfg = eval { PVE::Storage::config() };
+ return $consumers if !$cfg;
+
+ my $ids = $cfg->{ids} // {};
+ for my $storeid (sort keys %$ids) {
+ my $scfg = $ids->{$storeid};
+ next if ($scfg->{type} // '') ne 'lvm' || !defined($scfg->{base});
+
+ my ($baseid, $wwid) = eval { PVE::Storage::parse_volume_id($scfg->{base}, 1) };
+ next if !defined($baseid) || !defined($wwid);
+
+ my $basecfg = $ids->{$baseid};
+ next if !$basecfg || ($basecfg->{type} // '') ne 'multipath';
+
+ $consumers->{$wwid} = $storeid;
+ }
+
+ return $consumers;
+}
+
+# multipathd resolves an alias to a map name, so two WWIDs sharing one alias makes it drop a map
+# (the loser is order-dependent and only logged at level 1). Reject a collision up front.
+my sub assert_alias_free {
+ my ($cfg, $wwid, $alias) = @_;
+
+ for my $other (keys $cfg->{ids}->%*) {
+ next if $other eq $wwid;
+ my $section = $cfg->{ids}->{$other};
+ next if ($section->{type} // '') ne 'wwid';
+ die "alias '$alias' is already assigned to WWID '$other'\n"
+ if defined($section->{alias}) && $section->{alias} eq $alias;
+ }
+}
+
+# Apply a section update: set the given properties, then unset those named in $delete. Guard $delete
+# to the settable options and never set and delete the same key, so a stray 'delete=type' cannot
+# strip the section type and corrupt the file (as PVE::API2::Storage::Config guards it too).
+my sub apply_section_update {
+ my ($section, $param, $delete, $settable) = @_;
+
+ my @delete = split(/,/, $delete // '');
+ for my $key (@delete) {
+ raise_param_exc({ delete => "'$key' is not a settable property" }) if !$settable->{$key};
+ raise_param_exc({ $key => "cannot set and delete a property at the same time" })
+ if defined($param->{$key});
+ }
+ $section->{$_} = $param->{$_} for keys %$param;
+ delete $section->{$_} for @delete;
+}
+
+__PACKAGE__->register_method({
+ name => 'read',
+ path => '',
+ method => 'GET',
+ description => "Read the cluster-wide multipath configuration.",
+ permissions => {
+ check => ['perm', '/', ['Sys.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {},
+ },
+ returns => {
+ type => 'object',
+ properties => {
+ defaults => {
+ type => 'object',
+ description => 'Effective global multipathd knobs.',
+ additionalProperties => 1,
+ },
+ wwids => {
+ type => 'array',
+ description => 'The multipath WWID allow-list.',
+ items => { type => 'string' },
+ },
+ aliases => {
+ type => 'object',
+ description => 'Per-WWID human-readable aliases.',
+ additionalProperties => { type => 'string' },
+ },
+ overrides => {
+ type => 'string',
+ description => 'Verbatim multipath.conf override sections.',
+ optional => 1,
+ },
+ },
+ },
+ code => sub {
+ my $cfg = PVE::Multipath::ClusterConfig::read_config();
+ my $overrides = PVE::Multipath::ClusterConfig::read_overrides();
+ return {
+ defaults => PVE::Multipath::Config::effective_defaults($cfg),
+ wwids => PVE::Multipath::Config::wwid_list($cfg),
+ aliases => PVE::Multipath::Config::aliases($cfg),
+ defined($overrides) && length($overrides) ? (overrides => $overrides) : (),
+ };
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'set_overrides',
+ path => '',
+ method => 'PUT',
+ protected => 1,
+ description => "Set the verbatim multipath.conf override sections, such as "
+ . "hardware-specific 'device {}' or 'overrides {}' blocks.",
+ permissions => {
+ check => ['perm', '/', ['Sys.Modify']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ overrides => {
+ type => 'string',
+ description => 'Verbatim override sections. Omit to clear them.',
+ maxLength => 128 * 1024,
+ optional => 1,
+ },
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $overrides = $param->{overrides};
+ eval { PVE::Multipath::Config::check_overrides($overrides) };
+ raise_param_exc({ overrides => $@ }) if $@;
+
+ PVE::Multipath::ClusterConfig::write_overrides($overrides // '');
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'set_defaults',
+ path => 'defaults',
+ method => 'PUT',
+ protected => 1,
+ description => "Set the global multipathd defaults applied on every node.",
+ permissions => {
+ check => ['perm', '/', ['Sys.Modify']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ PVE::Multipath::Config::defaults_api_schema()->%*,
+ delete => {
+ type => 'string',
+ description => 'A list of settings to reset to their default, comma-separated.',
+ optional => 1,
+ },
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $delete = extract_param($param, 'delete');
+ my $settable = PVE::Multipath::Config::defaults_api_schema();
+
+ PVE::Multipath::ClusterConfig::lock_config(
+ sub {
+ my $cfg = PVE::Multipath::ClusterConfig::read_config();
+ my $section = $cfg->{ids}->{defaults} //= { type => 'defaults' };
+ apply_section_update($section, $param, $delete, $settable);
+ PVE::Multipath::ClusterConfig::write_config($cfg);
+ },
+ "updating multipath defaults failed",
+ );
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'add_wwid',
+ path => 'wwid',
+ method => 'POST',
+ protected => 1,
+ description => "Add a WWID to the multipath allow-list.",
+ permissions => {
+ check => ['perm', '/', ['Sys.Modify']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ wwid => {
+ type => 'string',
+ description => 'The WWID of the LUN to manage via multipath.',
+ maxLength => 128,
+ },
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $wwid = $param->{wwid};
+ raise_param_exc({ wwid => "does not look like a valid WWID" })
+ if $wwid !~ $WWID_RE;
+
+ PVE::Multipath::ClusterConfig::lock_config(
+ sub {
+ my $cfg = PVE::Multipath::ClusterConfig::read_config();
+ return if $cfg->{ids}->{$wwid};
+ $cfg->{ids}->{$wwid} = { type => 'wwid' };
+ PVE::Multipath::ClusterConfig::write_config($cfg);
+ },
+ "adding WWID '$wwid' failed",
+ );
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'update_wwid',
+ path => 'wwid/{wwid}',
+ method => 'PUT',
+ protected => 1,
+ description => "Set the alias and per-LUN multipathd knobs for an allow-listed WWID.",
+ permissions => {
+ check => ['perm', '/', ['Sys.Modify']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ wwid => {
+ type => 'string',
+ description => 'The WWID to update.',
+ maxLength => 128,
+ },
+ PVE::Multipath::Config::wwid_api_schema()->%*,
+ delete => {
+ type => 'string',
+ description => 'A list of settings to unset, comma-separated.',
+ optional => 1,
+ },
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $wwid = extract_param($param, 'wwid');
+ my $delete = extract_param($param, 'delete');
+ my $settable = PVE::Multipath::Config::wwid_api_schema();
+
+ PVE::Multipath::ClusterConfig::lock_config(
+ sub {
+ my $cfg = PVE::Multipath::ClusterConfig::read_config();
+ my $section = $cfg->{ids}->{$wwid};
+ die "WWID '$wwid' is not on the allow-list, add it first\n" if !$section;
+
+ assert_alias_free($cfg, $wwid, $param->{alias}) if defined($param->{alias});
+
+ apply_section_update($section, $param, $delete, $settable);
+ PVE::Multipath::ClusterConfig::write_config($cfg);
+ },
+ "updating WWID '$wwid' failed",
+ );
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'remove_wwid',
+ path => 'wwid/{wwid}',
+ method => 'DELETE',
+ protected => 1,
+ description => "Remove a WWID from the multipath allow-list.",
+ permissions => {
+ check => ['perm', '/', ['Sys.Modify']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ wwid => {
+ type => 'string',
+ description => 'The WWID to remove.',
+ maxLength => 128,
+ },
+ force => {
+ type => 'boolean',
+ description => 'Remove even while a storage still consumes the LUN.',
+ optional => 1,
+ default => 0,
+ },
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $wwid = $param->{wwid};
+
+ # dropping a consumed WWID flushes its map on the next sync, so the backing storage loses
+ # the device; refuse unless explicitly forced
+ if (!$param->{force}) {
+ my $storeid = multipath_consumers()->{$wwid};
+ die "WWID '$wwid' is still used by storage '$storeid', remove that"
+ . " storage first or pass 'force' to override\n"
+ if defined($storeid);
+ }
+
+ PVE::Multipath::ClusterConfig::lock_config(
+ sub {
+ my $cfg = PVE::Multipath::ClusterConfig::read_config();
+ delete $cfg->{ids}->{$wwid};
+ PVE::Multipath::ClusterConfig::write_config($cfg);
+ },
+ "removing WWID '$wwid' failed",
+ );
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'set_alias',
+ path => 'alias',
+ method => 'POST',
+ protected => 1,
+ description => "Set or replace the human-readable alias for an allow-listed WWID."
+ . " The alias becomes the dm-multipath map name on every node.",
+ permissions => {
+ check => ['perm', '/', ['Sys.Modify']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ wwid => {
+ type => 'string',
+ description => 'The WWID to label.',
+ maxLength => 128,
+ },
+ alias => PVE::Multipath::Config::wwid_api_schema()->{alias},
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $wwid = $param->{wwid};
+ my $alias = $param->{alias};
+ raise_param_exc({ wwid => "does not look like a valid WWID" })
+ if $wwid !~ $WWID_RE;
+
+ PVE::Multipath::ClusterConfig::lock_config(
+ sub {
+ my $cfg = PVE::Multipath::ClusterConfig::read_config();
+ die "WWID '$wwid' is not on the allow-list, add it first\n"
+ if !$cfg->{ids}->{$wwid};
+ assert_alias_free($cfg, $wwid, $alias);
+ $cfg->{ids}->{$wwid}->{alias} = $alias;
+ PVE::Multipath::ClusterConfig::write_config($cfg);
+ },
+ "setting alias for WWID '$wwid' failed",
+ );
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'remove_alias',
+ path => 'alias/{wwid}',
+ method => 'DELETE',
+ protected => 1,
+ description => "Remove the alias for a multipath WWID.",
+ permissions => {
+ check => ['perm', '/', ['Sys.Modify']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ wwid => {
+ type => 'string',
+ description => 'The WWID whose alias to remove.',
+ maxLength => 128,
+ },
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $wwid = $param->{wwid};
+ PVE::Multipath::ClusterConfig::lock_config(
+ sub {
+ my $cfg = PVE::Multipath::ClusterConfig::read_config();
+ delete $cfg->{ids}->{$wwid}->{alias} if $cfg->{ids}->{$wwid};
+ PVE::Multipath::ClusterConfig::write_config($cfg);
+ },
+ "removing alias for WWID '$wwid' failed",
+ );
+ return undef;
+ },
+});
+
+1;
--
2.47.3
next prev parent reply other threads:[~2026-06-26 12:10 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-26 12:07 [PATCH storage,cluster,manager 0/13] multipath: cluster-wide config, storage and health overview Thomas Lamprecht
2026-06-26 12:07 ` [PATCH storage 01/13] multipath: add helper library and managed configuration Thomas Lamprecht
2026-06-26 14:43 ` Maximiliano Sandoval
2026-06-26 12:07 ` [PATCH storage 02/13] api: disks: add read-only multipath status endpoint Thomas Lamprecht
2026-06-26 12:07 ` Thomas Lamprecht [this message]
2026-06-26 12:07 ` [PATCH storage 04/13] multipath: add storage plugin for multipath LUNs Thomas Lamprecht
2026-06-26 12:07 ` [PATCH storage 05/13] lvm: allow a multipath storage as the base device Thomas Lamprecht
2026-06-26 12:07 ` [PATCH storage 06/13] multipath: broadcast per-node map health to the cluster KV store Thomas Lamprecht
2026-06-26 12:07 ` [PATCH storage 07/13] api: multipath: add cluster-wide health status endpoint Thomas Lamprecht
2026-06-26 12:07 ` [PATCH cluster 08/13] pmxcfs: track cluster-wide multipath configuration Thomas Lamprecht
2026-06-26 12:07 ` [PATCH manager 09/13] pvestatd: apply the cluster-wide multipath config on each node Thomas Lamprecht
2026-06-26 12:07 ` [PATCH manager 10/13] api: cluster: mount the multipath configuration endpoint Thomas Lamprecht
2026-06-26 12:07 ` [PATCH manager 11/13] pvestatd: broadcast multipath map health to the cluster Thomas Lamprecht
2026-06-26 12:07 ` [PATCH manager 12/13] ui: dc: add multipath health matrix and config editor Thomas Lamprecht
2026-06-26 14:05 ` Maximiliano Sandoval
2026-06-26 12:07 ` [PATCH manager 13/13] ui: node: show multipath maps and their paths under Disks 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=20260626121000.2095591-4-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.