public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dietmar Maurer <dietmar@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [RFC pve-storage 10/27] diskmanage: add helper querying multipath path state
Date: Fri, 31 Jul 2026 12:21:39 +0200	[thread overview]
Message-ID: <20260731102156.3947857-11-dietmar@proxmox.com> (raw)
In-Reply-To: <20260731102156.3947857-1-dietmar@proxmox.com>

Only multipathd knows the health of member paths: a faulty path stays
in the device mapper table (and thus in the sysfs slave list) until
the transport removes the device, so the slave count alone cannot
detect a degraded map. Parse "multipathd show maps json" into per-map
active path counts, fault counters and per-path states, keyed by
WWID. ALUA standby paths count as active, only paths the kernel
failed are unusable.

Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
 src/PVE/Diskmanage.pm                         |  57 ++++++++
 src/test/disk_tests/multipath/multipathd      | 124 ++++++++++++++++++
 .../multipath/multipathd_status_expected.json |  42 ++++++
 src/test/disklist_test.pm                     |  20 +++
 4 files changed, 243 insertions(+)
 create mode 100644 src/test/disk_tests/multipath/multipathd
 create mode 100644 src/test/disk_tests/multipath/multipathd_status_expected.json

diff --git a/src/PVE/Diskmanage.pm b/src/PVE/Diskmanage.pm
index ab5db06..a6730a6 100644
--- a/src/PVE/Diskmanage.pm
+++ b/src/PVE/Diskmanage.pm
@@ -20,6 +20,7 @@ my $SGDISK = "/sbin/sgdisk";
 my $PVS = "/sbin/pvs";
 my $LVS = "/sbin/lvs";
 my $LSBLK = "/bin/lsblk";
+my $MULTIPATHD = "/sbin/multipathd";
 
 my sub strip_dev : prototype($) {
     my ($devpath) = @_;
@@ -912,6 +913,62 @@ sub get_multipath_disks {
     return $disklist;
 }
 
+# Queries multipathd for the path state of all multipath mapped devices, keyed by their WWID.
+# Only the daemon knows the health of the member paths; a faulty path stays in the device
+# mapper table (and thus in the sysfs slave list) until the transport removes the device.
+# Dies if multipathd is not available.
+sub get_multipath_status {
+    die "multipathd is not available\n" if !check_bin($MULTIPATHD);
+
+    my $output = '';
+    run_command(
+        [$MULTIPATHD, 'show', 'maps', 'json'],
+        outfunc => sub { $output .= "$_[0]\n"; },
+        timeout => 5,
+    );
+
+    my $parsed = decode_json($output);
+
+    my $res = {};
+    for my $map (@{ $parsed->{maps} // [] }) {
+        my $wwid = $map->{uuid} // next;
+
+        my $paths = [];
+        my $active = 0;
+        for my $group (@{ $map->{path_groups} // [] }) {
+            for my $path (@{ $group->{paths} // [] }) {
+                my $dev = $path->{dev} // next;
+                my $state = $path->{dm_st} // 'undef';
+                # ALUA standby paths are 'active' with a 'ghost' checker state, only
+                # paths the kernel actually failed count as unusable
+                $active++ if $state ne 'failed';
+
+                my $entry = { device => "/dev/$dev", state => $state };
+                $entry->{'checker-state'} = $path->{chk_st} if defined($path->{chk_st});
+                my $wwpns = {
+                    'host-wwpn' => $path->{host_wwpn},
+                    'target-wwpn' => $path->{target_wwpn},
+                };
+                for my $key (sort keys %$wwpns) {
+                    my $wwpn = $wwpns->{$key} // next;
+                    # multipathd reports '[undef]' for transports without a WWPN
+                    next if $wwpn eq '' || $wwpn eq '[undef]';
+                    $entry->{$key} = $wwpn;
+                }
+                push @$paths, $entry;
+            }
+        }
+
+        $res->{$wwid} = {
+            active => $active,
+            faults => $map->{path_faults} // 0,
+            paths => $paths,
+        };
+    }
+
+    return $res;
+}
+
 # Returns the multipath device entry if the given path refers to a multipath mapped device.
 sub lookup_multipath_map {
     my ($devpath) = @_;
diff --git a/src/test/disk_tests/multipath/multipathd b/src/test/disk_tests/multipath/multipathd
new file mode 100644
index 0000000..40243f9
--- /dev/null
+++ b/src/test/disk_tests/multipath/multipathd
@@ -0,0 +1,124 @@
+{
+   "major_version": 0,
+   "minor_version": 1,
+   "maps": [{
+      "name" : "mpatha",
+      "uuid" : "3600140500000000000000000000000001",
+      "sysfs" : "dm-0",
+      "failback" : "immediate",
+      "queueing" : "5 chk",
+      "paths" : 2,
+      "write_prot" : "rw",
+      "dm_st" : "active",
+      "features" : "1 queue_if_no_path",
+      "hwhandler" : "1 alua",
+      "action" : "",
+      "path_faults" : 0,
+      "vend" : "SANVEND ",
+      "prod" : "SANMODEL",
+      "rev" : "4.1",
+      "switch_grp" : 0,
+      "map_loads" : 1,
+      "total_q_time" : 0,
+      "q_timeouts" : 0,
+      "path_groups": [{
+         "selector" : "service-time 0",
+         "pri" : 50,
+         "dm_st" : "active",
+         "marginal_st" : "normal",
+         "group" : 1,
+         "paths": [{
+            "dev" : "sda",
+            "dev_t" : "8:0",
+            "dm_st" : "active",
+            "dev_st" : "running",
+            "chk_st" : "ready",
+            "checker" : "tur",
+            "pri" : 50,
+            "host_wwnn" : "0x20000024ff7d6a1c",
+            "target_wwnn" : "0x2000d039ea28bf00",
+            "host_wwpn" : "0x21000024ff7d6a1c",
+            "target_wwpn" : "0x2100d039ea28bf00",
+            "host_adapter" : "0000:41:00.0",
+            "lun_hex" : "0x0001000000000000",
+            "marginal_st" : "normal"
+         },
+         {
+            "dev" : "sdb",
+            "dev_t" : "8:16",
+            "dm_st" : "active",
+            "dev_st" : "running",
+            "chk_st" : "ready",
+            "checker" : "tur",
+            "pri" : 50,
+            "host_wwnn" : "0x20000024ff7d6a1d",
+            "target_wwnn" : "0x2000d039ea28bf00",
+            "host_wwpn" : "0x21000024ff7d6a1d",
+            "target_wwpn" : "0x2200d039ea28bf00",
+            "host_adapter" : "0000:41:00.1",
+            "lun_hex" : "0x0001000000000000",
+            "marginal_st" : "normal"
+         }]
+      }]
+   },
+   {
+      "name" : "mpathb",
+      "uuid" : "3600140500000000000000000000000002",
+      "sysfs" : "dm-1",
+      "failback" : "immediate",
+      "queueing" : "5 chk",
+      "paths" : 2,
+      "write_prot" : "rw",
+      "dm_st" : "active",
+      "features" : "1 queue_if_no_path",
+      "hwhandler" : "1 alua",
+      "action" : "",
+      "path_faults" : 3,
+      "vend" : "SANVEND ",
+      "prod" : "SANMODEL",
+      "rev" : "4.1",
+      "switch_grp" : 0,
+      "map_loads" : 1,
+      "total_q_time" : 0,
+      "q_timeouts" : 0,
+      "path_groups": [{
+         "selector" : "service-time 0",
+         "pri" : 50,
+         "dm_st" : "active",
+         "marginal_st" : "normal",
+         "group" : 1,
+         "paths": [{
+            "dev" : "sdc",
+            "dev_t" : "8:32",
+            "dm_st" : "active",
+            "dev_st" : "running",
+            "chk_st" : "ready",
+            "checker" : "tur",
+            "pri" : 50,
+            "host_wwnn" : "0x20000024ff7d6a1c",
+            "target_wwnn" : "0x2000d039ea28bf00",
+            "host_wwpn" : "0x21000024ff7d6a1c",
+            "target_wwpn" : "0x2100d039ea28bf00",
+            "host_adapter" : "0000:41:00.0",
+            "lun_hex" : "0x0002000000000000",
+            "marginal_st" : "normal"
+         },
+         {
+            "dev" : "sdd",
+            "dev_t" : "8:48",
+            "dm_st" : "failed",
+            "dev_st" : "running",
+            "chk_st" : "faulty",
+            "checker" : "tur",
+            "pri" : 0,
+            "host_wwnn" : "0x20000024ff7d6a1d",
+            "target_wwnn" : "0x2000d039ea28bf00",
+            "host_wwpn" : "0x21000024ff7d6a1d",
+            "target_wwpn" : "0x2200d039ea28bf00",
+            "host_adapter" : "0000:41:00.1",
+            "lun_hex" : "0x0002000000000000",
+            "marginal_st" : "normal"
+         }]
+      }]
+   }]
+}
diff --git a/src/test/disk_tests/multipath/multipathd_status_expected.json b/src/test/disk_tests/multipath/multipathd_status_expected.json
new file mode 100644
index 0000000..d57e620
--- /dev/null
+++ b/src/test/disk_tests/multipath/multipathd_status_expected.json
@@ -0,0 +1,42 @@
+{
+    "3600140500000000000000000000000001": {
+        "active": 2,
+        "faults": 0,
+        "paths": [
+            {
+                "device": "/dev/sda",
+                "state": "active",
+                "checker-state": "ready",
+                "host-wwpn": "0x21000024ff7d6a1c",
+                "target-wwpn": "0x2100d039ea28bf00"
+            },
+            {
+                "device": "/dev/sdb",
+                "state": "active",
+                "checker-state": "ready",
+                "host-wwpn": "0x21000024ff7d6a1d",
+                "target-wwpn": "0x2200d039ea28bf00"
+            }
+        ]
+    },
+    "3600140500000000000000000000000002": {
+        "active": 1,
+        "faults": 3,
+        "paths": [
+            {
+                "device": "/dev/sdc",
+                "state": "active",
+                "checker-state": "ready",
+                "host-wwpn": "0x21000024ff7d6a1c",
+                "target-wwpn": "0x2100d039ea28bf00"
+            },
+            {
+                "device": "/dev/sdd",
+                "state": "failed",
+                "checker-state": "faulty",
+                "host-wwpn": "0x21000024ff7d6a1d",
+                "target-wwpn": "0x2200d039ea28bf00"
+            }
+        ]
+    }
+}
diff --git a/src/test/disklist_test.pm b/src/test/disklist_test.pm
index d2ea3b2..e804485 100644
--- a/src/test/disklist_test.pm
+++ b/src/test/disklist_test.pm
@@ -68,6 +68,8 @@ sub mocked_run_command {
                 $content = '{}';
             }
             @$outputlines = split(/\n/, $content);
+        } elsif ($cmd->[0] =~ m/multipathd/i) {
+            @$outputlines = split(/\n/, read_test_file('multipathd'));
         } else {
             die "unexpected run_command call: '@$cmd', aborting\n";
         }
@@ -257,6 +259,16 @@ sub test_disk_list {
             like($@, qr/not a valid local disk/, 'unknown device mapper device should fail');
         }
 
+        if (-f "disk_tests/$testcasedir/multipathd_status_expected.json") {
+            my $status = PVE::Diskmanage::get_multipath_status();
+            my $expected_status =
+                decode_json(read_test_file('multipathd_status_expected.json'));
+
+            print Dumper($status) if $print;
+            $testcount++;
+            is_deeply($status, $expected_status, 'multipath status should be the same');
+        }
+
         done_testing($testcount);
     };
 }
@@ -279,6 +291,14 @@ $diskmanage_module->mock('get_sysdir_size' => \&mocked_get_sysdir_size);
 print("\tMocked get_sysdir_size\n");
 $diskmanage_module->mock('assert_blockdev' => sub { return 1; });
 print("\tMocked assert_blockdev\n");
+$diskmanage_module->mock(
+    'check_bin' => sub {
+        my ($path) = @_;
+        return 1 if $path =~ m/multipathd/;
+        return &{ $diskmanage_module->original('check_bin') }($path);
+    },
+);
+print("\tMocked check_bin\n");
 $diskmanage_module->mock(
     'dir_is_empty' => sub {
         my $val = shift;
-- 
2.47.3




  parent reply	other threads:[~2026-07-31 10:22 UTC|newest]

Thread overview: 28+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-31 10:21 [RFC pve-storage/proxmox-widget-toolkit/pve-manager 00/27] add guided remote storage setup and SAN visibility Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 01/27] diskmanage: collect disk transport type from lsblk Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 02/27] diskmanage: add helper to list multipath devices Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 03/27] diskmanage: qualify NVMe over fabrics transport Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 04/27] disks: list: add include-remote parameter Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 05/27] diskmanage: include iSCSI session devices in disk enumeration Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 06/27] diskmanage: link multipath member disks to their map device Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 07/27] iscsi: factor out session device map from device list Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 08/27] api: scan: add san-luns method listing SAN LUN candidates Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 09/27] disks: lvm: allow creating volume groups on multipath devices Dietmar Maurer
2026-07-31 10:21 ` Dietmar Maurer [this message]
2026-07-31 10:21 ` [RFC pve-storage 11/27] diskmanage: add helper querying NVMe native multipath path state Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 12/27] api: scan: san-luns: report " Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 13/27] iscsi plugin: list sessions of all transports and capture transport Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 14/27] api: add node-level iSCSI initiator target and session API Dietmar Maurer
2026-07-31 10:21 ` [RFC proxmox-widget-toolkit 15/27] disk selectors: allow opting into remote devices Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 16/27] ui: storage: allow switching the scan node of the NFS/CIFS scan combos Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 17/27] ui: storage: add guided remote storage wizard with NFS support Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 18/27] ui: storage wizard: add SMB/CIFS support Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 19/27] ui: storage wizard: add iSCSI support Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 20/27] ui: storage wizard: add FC-attached SAN (shared LVM) support Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 21/27] ui: storage wizard: add ZFS over iSCSI support Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 22/27] ui: dc: storage: add remote storage wizard entry to the add menu Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 23/27] ui: node: add SAN LUNs panel Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 24/27] ui: san luns: show multipath path state Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 25/27] api: nodes: add iSCSI initiator API endpoint Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 26/27] pvenode: add iscsi commands Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 27/27] ui: san luns: show iSCSI targets and sessions Dietmar Maurer

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=20260731102156.3947857-11-dietmar@proxmox.com \
    --to=dietmar@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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal