Normalize legacy Unraid raw statuses

Refs #1400
This commit is contained in:
rcourtman
2026-04-15 16:46:20 +01:00
parent 31315fb594
commit 0fa1cf5fc1
3 changed files with 107 additions and 4 deletions
@@ -133,7 +133,12 @@ it exposes a direct CPU percentage and otherwise fall back to Podman's
wall-clock delta semantics rather than Docker's multi-core normalization, and
Proxmox Ceph status decoding must accept manager standby entries as either bare
names or structured objects so collector payload variations do not break the
canonical monitoring path.
canonical monitoring path. That same compatibility boundary also owns legacy
Unraid raw-status normalization at host-agent ingest: when older agents send
`rawStatus` without the newer normalized `status`, `internal/monitoring/monitor_agents.go`
must derive the canonical disk status before storage-risk assessment runs so
v5 aggregate counters do not override clearly healthy per-disk state during v6
compatibility operation.
VMware vSphere now also has a locked phase-1 ingestion boundary under this
lane. The admitted direction is vCenter-only in phase 1, and monitoring must
stay API-first through the
+34 -3
View File
@@ -1740,12 +1740,18 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config.
if report.Unraid != nil {
disks := make([]models.HostUnraidDisk, 0, len(report.Unraid.Disks))
for _, disk := range report.Unraid.Disks {
device := strings.TrimSpace(disk.Device)
rawStatus := strings.TrimSpace(disk.RawStatus)
status := strings.TrimSpace(disk.Status)
if status == "" {
status = normalizeLegacyUnraidDiskStatus(rawStatus, device)
}
disks = append(disks, models.HostUnraidDisk{
Name: strings.TrimSpace(disk.Name),
Device: strings.TrimSpace(disk.Device),
Device: device,
Role: strings.TrimSpace(disk.Role),
Status: strings.TrimSpace(disk.Status),
RawStatus: strings.TrimSpace(disk.RawStatus),
Status: status,
RawStatus: rawStatus,
Serial: strings.TrimSpace(disk.Serial),
Filesystem: strings.TrimSpace(disk.Filesystem),
SizeBytes: disk.SizeBytes,
@@ -2063,6 +2069,31 @@ func normalizeHostDiskDevice(device string) string {
return strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(device), "/dev/"))
}
func normalizeLegacyUnraidDiskStatus(rawStatus, device string) string {
status := strings.ToUpper(strings.TrimSpace(rawStatus))
switch {
case status == "":
if strings.TrimSpace(device) != "" {
return "online"
}
return ""
case strings.Contains(status, "DISK_OK") || status == "OK":
return "online"
case strings.Contains(status, "DISK_DSBL") || strings.Contains(status, "DISABLED"):
return "disabled"
case strings.Contains(status, "DISK_NP") || strings.Contains(status, "MISSING") || strings.Contains(status, "NOT_INSTALLED"):
return "missing"
case strings.Contains(status, "DISK_INVALID") || strings.Contains(status, "INVALID"):
return "invalid"
case strings.Contains(status, "DISK_WRONG") || strings.Contains(status, "WRONG"):
return "wrong"
case strings.Contains(status, "DISK_ERROR") || strings.Contains(status, "ERROR"):
return "error"
default:
return strings.ToLower(status)
}
}
func hostDiskIOMetricResourceID(host models.Host, io models.DiskIO) string {
device := normalizeHostDiskDevice(io.Device)
if device == "" {
@@ -8,6 +8,7 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/storagehealth"
agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
"github.com/rcourtman/pulse-go-rewrite/pkg/metrics"
)
@@ -618,6 +619,72 @@ func TestApplyHostReportStoresUnraidTopology(t *testing.T) {
}
}
func TestApplyHostReportNormalizesLegacyUnraidRawStatuses(t *testing.T) {
t.Helper()
monitor := &Monitor{
state: models.NewState(),
alertManager: alerts.NewManager(),
hostTokenBindings: make(map[string]string),
config: &config.Config{},
rateTracker: NewRateTracker(),
}
t.Cleanup(func() { monitor.alertManager.Stop() })
report := agentshost.Report{
Agent: agentshost.AgentInfo{
ID: "agent-tower-legacy",
Version: "5.1.27",
IntervalSeconds: 30,
},
Host: agentshost.HostInfo{
ID: "machine-tower-legacy",
Hostname: "tower",
MachineID: "machine-tower-legacy",
},
Metrics: agentshost.Metrics{
Memory: agentshost.MemoryMetric{TotalBytes: 1024, UsedBytes: 512, FreeBytes: 512, Usage: 50},
},
Unraid: &agentshost.UnraidStorage{
ArrayStarted: true,
ArrayState: "STARTED",
SyncAction: "check",
SyncProgress: 55,
NumDisabled: 1,
NumInvalid: 1,
Disks: []agentshost.UnraidDisk{
{Name: "parity", Device: "/dev/sdb", Role: "parity", RawStatus: "DISK_OK", Serial: "SERIAL-PARITY"},
{Name: "disk1", Device: "/dev/sdc", Role: "data", RawStatus: "DISK_OK", Serial: "SERIAL-DATA"},
},
},
Timestamp: time.Now().UTC(),
}
host, err := monitor.ApplyHostReport(report, nil)
if err != nil {
t.Fatalf("ApplyHostReport: %v", err)
}
if host.Unraid == nil {
t.Fatal("expected unraid topology on host")
}
for _, disk := range host.Unraid.Disks {
if disk.Status != "online" {
t.Fatalf("expected normalized online status from rawStatus, got %+v", host.Unraid.Disks)
}
}
assessment := storagehealth.AssessUnraidStorage(*host.Unraid)
if assessment.Level != storagehealth.RiskWarning {
t.Fatalf("assessment level = %q, want %q", assessment.Level, storagehealth.RiskWarning)
}
for _, reason := range assessment.Reasons {
if reason.Code == "unraid_disabled_disks" || reason.Code == "unraid_invalid_disks" {
t.Fatalf("unexpected aggregate-count reason after raw-status normalization: %+v", assessment.Reasons)
}
}
}
func TestApplyHostReportFiltersVendorManagedSystemRAIDArrays(t *testing.T) {
t.Helper()