Show ZFS pool layout, scrub status and device health in Storage view

/api/storage now runs a single `zpool status -j --json-int -P` (OpenZFS
2.3+) for all pools and returns, per pool:
- top-level vdevs with type, raidz/draid parity, class (data, special,
  dedup, log, cache, spare), state and read/write/checksum errors
- member devices with path, resolved base disk, state, error counters,
  slow I/Os, and the spare-N/replacing-N they sit under while swapping
- scrub/resilver status (progress only while running), the pool's
  status/action text and data-error count
- fragmentation, capacity and dedup ratio from `zpool list`

This replaces the per-pool `zpool status` text loop whose result
(zfs_disks) was never used. Member paths (by-id, by-partlabel, wwn) are
resolved to base disks via sysfs rather than an lsblk call per device.
On ZFS without `-j`, pools report details_available: false and the UI
falls back to the existing basic card.

The ZFS pool card shows a layout summary (e.g. "RAIDZ2 | 5 disks |
Scrub finished ...") and opens a details dialog with the vdev tree,
per-device error counts, scrub/resilver progress, and links from each
member disk to its disk details dialog.

Tested against `zpool status -j` output from PVE 9 / OpenZFS 2.4.4:
mirror, raidz2, striped mirrors, single-disk pool, and a file-backed
degraded raidz1 with log, cache and spare devices.

Tests cover the `zpool status -j` -> Storage view shape against real
output from PVE 9 / OpenZFS 2.4.4 hosts (serials and WWNs replaced):
mirror, raidz2, single-disk pool, slow I/Os, and a degraded raidz1 with
log, cache and spare devices; plus a synthetic resilver with a spare
swapping in. Member path -> base disk resolution is tested against a
stubbed sysfs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DrOwl
2026-10-29 11:55:02 +00:00
co-authored by Claude Opus 5.5
parent 5db3ae9c92
commit 72491b2445
7 changed files with 1308 additions and 19 deletions
@@ -0,0 +1,69 @@
{
"output_version": {
"command": "zpool status",
"vers_major": 0,
"vers_minor": 1
},
"pools": {
"rpool": {
"name": "rpool",
"state": "ONLINE",
"txg": 38071404,
"spa_version": 5000,
"zpl_version": 5,
"scan_stats": {
"function": "SCRUB",
"state": "FINISHED",
"start_time": 1791674641,
"end_time": 1791674884,
"to_examine": 36167286784,
"examined": 36193234944,
"skipped": 6365184,
"processed": 0,
"errors": 0,
"bytes_per_scan": 0,
"pass_start": 1791909484,
"scrub_pause": 0,
"scrub_spent_paused": 0,
"issued_bytes_per_scan": 0,
"issued": 36186869760
},
"vdevs": {
"rpool": {
"name": "rpool",
"vdev_type": "root",
"class": "normal",
"state": "ONLINE",
"alloc_space": 37495803904,
"total_space": 476741369856,
"def_space": 476741369856,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"vdevs": {
"/dev/disk/by-id/ata-Patriot_Burst_00000000000000000001-part3": {
"name": "/dev/disk/by-id/ata-Patriot_Burst_00000000000000000001-part3",
"vdev_type": "disk",
"path": "/dev/disk/by-id/ata-Patriot_Burst_00000000000000000001-part3",
"class": "normal",
"state": "ONLINE",
"alloc_space": 37495803904,
"total_space": 476741369856,
"def_space": 476741369856,
"rep_dev_size": 476746088448,
"phys_space": 479565971456,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
}
},
"guid": 1001
}
},
"error_count": 0,
"pool_guid": 1000
}
}
}