Normalize Proxmox-reported disk health to the canonical vocabulary

PVE's disks/list endpoint labels healthy SCSI/SAS drives OK while ATA
drives say PASSED, and failing ATA drives come back as FAILED! with the
bang. Pulse ingested those raw strings, so a healthy SAS drive rendered
as health Unknown even though the Proxmox UI showed S.M.A.R.T. OK. The
host agent and TrueNAS paths already normalize their health text; the
PVE ingestion was the only entry that did not.

Map OK/PASS to PASSED and FAIL-containing values to FAILED at ingestion,
and accept OK as healthy in the disk presentation layer as defense for
state produced by older servers.

Refs #1595

Contract-Neutral: behavioral fix: normalize PVE disk health strings at ingestion, no public contract delta (#1595)
This commit is contained in:
rcourtman
2026-07-21 20:30:54 +01:00
parent 38ecb2e1a0
commit 0e6fa57c08
4 changed files with 60 additions and 6 deletions
@@ -234,6 +234,8 @@ describe('diskPresentation', () => {
}),
).toBe('Pending sectors detected.');
expect(getPhysicalDiskHealthStatus(makeDiskData({ health: 'UNKNOWN' })).label).toBe('Unknown');
// PVE reports SCSI/SAS drives as OK; older servers pass it through raw (#1595)
expect(getPhysicalDiskHealthStatus(makeDiskData({ health: 'OK' })).label).toBe('Healthy');
expect(
getPhysicalDiskEmptyStatePresentation({
@@ -188,6 +188,10 @@ const PHYSICAL_DISK_BAD_HEALTH_STATES = new Set([
'UNHEALTHY',
]);
// PVE reports SCSI/SAS drives as OK (ATA drives say PASSED); older server
// builds pass that raw value through, so accept it here as well (#1595).
const PHYSICAL_DISK_HEALTHY_STATES = new Set(['PASSED', 'GOOD', 'OK']);
const normalizePhysicalDiskState = (value: string | undefined | null): string =>
(value || '').trim().toLowerCase();
@@ -559,11 +563,10 @@ export function getPhysicalDiskHealthStatus(
}
return {
label: normalizedHealth === 'PASSED' || normalizedHealth === 'GOOD' ? 'Healthy' : 'Unknown',
summary:
normalizedHealth === 'PASSED' || normalizedHealth === 'GOOD'
? 'No active disk-health issues.'
: 'Health state is not reported.',
label: PHYSICAL_DISK_HEALTHY_STATES.has(normalizedHealth) ? 'Healthy' : 'Unknown',
summary: PHYSICAL_DISK_HEALTHY_STATES.has(normalizedHealth)
? 'No active disk-health issues.'
: 'Health state is not reported.',
tone: 'text-base-content',
};
}
@@ -0,0 +1,30 @@
package monitoring
import "testing"
// PVE's disks/list endpoint reports ATA drives as PASSED/FAILED! and SCSI/SAS
// drives as OK or a failure sentence. The raw OK previously reached the UI
// untouched and rendered as Unknown (#1595).
func TestNormalizeProxmoxDiskHealth(t *testing.T) {
cases := []struct {
name string
in string
want string
}{
{"scsi ok", "OK", "PASSED"},
{"scsi ok lowercase", "ok", "PASSED"},
{"ata passed", "PASSED", "PASSED"},
{"ata failed bang", "FAILED!", "FAILED"},
{"scsi failure sentence", "FAILURE PREDICTION THRESHOLD EXCEEDED", "FAILED"},
{"unknown passthrough", "UNKNOWN", "UNKNOWN"},
{"empty passthrough", "", ""},
{"whitespace trimmed", " OK ", "PASSED"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := normalizeProxmoxDiskHealth(tc.in); got != tc.want {
t.Fatalf("normalizeProxmoxDiskHealth(%q) = %q, want %q", tc.in, got, tc.want)
}
})
}
}
+20 -1
View File
@@ -1033,7 +1033,7 @@ func (m *Monitor) maybePollPhysicalDisksAsync(
WWN: disk.WWN,
Type: disk.Type,
Size: disk.Size,
Health: disk.Health,
Health: normalizeProxmoxDiskHealth(disk.Health),
Wearout: disk.Wearout,
RPM: disk.RPM,
Used: disk.Used,
@@ -1120,6 +1120,25 @@ func (m *Monitor) maybePollPhysicalDisksAsync(
}(instanceName, client, nodes, nodeEffectiveStatus, modelNodes)
}
// normalizeProxmoxDiskHealth maps the raw health strings the Proxmox disks API
// reports onto the canonical PASSED/FAILED vocabulary the disk model carries.
// ATA drives come back as PASSED or FAILED!, while SCSI/SAS drives report OK
// or a failure sentence, and the raw OK previously rendered as Unknown in the
// UI (#1595). Unrecognized values pass through untouched so nothing real is
// masked.
func normalizeProxmoxDiskHealth(health string) string {
trimmed := strings.TrimSpace(health)
upper := strings.ToUpper(trimmed)
switch {
case upper == "OK", strings.Contains(upper, "PASS"):
return "PASSED"
case strings.Contains(upper, "FAIL"):
return "FAILED"
default:
return trimmed
}
}
// physicalDisksFromHostAgentSMART builds PhysicalDisk entries for a node from
// its linked host agent's SMART inventory. This is the fallback when the
// Proxmox disks/list query fails: PVE probes SMART per disk inside that call,