mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
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:
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user