seed DiskFillByType defaults and normalize on load

Seeds AlertConfig.DiskFillByType in defaultAlertConfig() with the three
forward-compatible keys (nvme 92/87, sata 90/85, hdd 85/80). Adds
NormalizeDiskFillByType to lowercase keys on load, seed defaults when
the map is nil, and reset non-positive trigger/clear values to the
default for that key. NormalizeAgentDefaults invokes the new helper so
it runs through the existing UpdateConfig normalization chain. Round
trip tests cover nil seed, customized survival, negative reset, and
mixed case normalization, plus a defaultAlertConfig seed assertion.

Only the "nvme" key is consulted by the host evaluation today (the
inference helper only matches /dev/nvme*); "sata" and "hdd" are
forward-compatible placeholders for when the agent protocol carries
hardware type explicitly.
This commit is contained in:
rcourtman
2026-05-13 06:35:54 +01:00
parent 85fbbd4ef6
commit 950bd187a9
3 changed files with 174 additions and 2 deletions
+55
View File
@@ -282,6 +282,61 @@ func NormalizeAgentDefaults(config *AlertConfig) {
}
}
EnsureValidHysteresis(config.AgentDefaults.DiskTemperature, "agent.diskTemperature")
NormalizeDiskFillByType(config)
}
// diskFillByTypeDefaults returns the canonical per-type fill-% defaults.
// Keys are lowercase hardware type strings.
func diskFillByTypeDefaults() map[string]HysteresisThreshold {
return map[string]HysteresisThreshold{
"nvme": {Trigger: 92, Clear: 87},
"sata": {Trigger: 90, Clear: 85},
"hdd": {Trigger: 85, Clear: 80},
}
}
// NormalizeDiskFillByType ensures AlertConfig.DiskFillByType is seeded with
// lowercase nvme/sata/hdd defaults when nil, lowercases any existing keys,
// and resets non-positive trigger or clear values to the default for that
// key. Operator-customized positive values are preserved.
func NormalizeDiskFillByType(config *AlertConfig) {
defaults := diskFillByTypeDefaults()
if config.DiskFillByType == nil {
copyMap := make(map[string]HysteresisThreshold, len(defaults))
for k, v := range defaults {
copyMap[k] = v
}
config.DiskFillByType = copyMap
return
}
// Lowercase any non-lowercase keys, moving values into the canonical position.
for key, value := range config.DiskFillByType {
lower := strings.ToLower(strings.TrimSpace(key))
if lower == key {
continue
}
delete(config.DiskFillByType, key)
if lower == "" {
continue
}
if _, exists := config.DiskFillByType[lower]; !exists {
config.DiskFillByType[lower] = value
}
}
// Ensure all canonical keys are present and have positive trigger/clear values.
for key, defaultVal := range defaults {
current, ok := config.DiskFillByType[key]
if !ok {
config.DiskFillByType[key] = defaultVal
continue
}
if current.Trigger <= 0 || current.Clear <= 0 {
config.DiskFillByType[key] = defaultVal
}
}
}
func NormalizeGeneralSettings(config *AlertConfig) {
+6 -1
View File
@@ -76,7 +76,12 @@ func defaultAlertConfig() AlertConfig {
CPU: &HysteresisThreshold{Trigger: 80, Clear: 75},
Memory: &HysteresisThreshold{Trigger: 85, Clear: 80},
},
StorageDefault: HysteresisThreshold{Trigger: 85, Clear: 80},
StorageDefault: HysteresisThreshold{Trigger: 85, Clear: 80},
DiskFillByType: map[string]HysteresisThreshold{
"nvme": {Trigger: 92, Clear: 87},
"sata": {Trigger: 90, Clear: 85},
"hdd": {Trigger: 85, Clear: 80},
},
MinimumDelta: 2.0, // 2% minimum change
SuppressionWindow: 5, // 5 minutes
HysteresisMargin: 5.0, // 5% default margin
+113 -1
View File
@@ -1,6 +1,10 @@
package alerts
import "testing"
import (
"testing"
alertconfig "github.com/rcourtman/pulse-go-rewrite/internal/alerts/config"
)
func TestNormalizeSnapshotDefaults(t *testing.T) {
tests := []struct {
@@ -189,3 +193,111 @@ func TestBackupIgnoreVMID(t *testing.T) {
})
}
}
func TestNormalizeDiskFillByType(t *testing.T) {
t.Run("nil map seeds nvme/sata/hdd defaults", func(t *testing.T) {
cfg := &AlertConfig{}
alertconfig.NormalizeDiskFillByType(cfg)
if cfg.DiskFillByType == nil {
t.Fatal("expected DiskFillByType to be seeded, got nil")
}
nvme, ok := cfg.DiskFillByType["nvme"]
if !ok {
t.Fatal("nvme key not seeded")
}
if nvme.Trigger != 92 || nvme.Clear != 87 {
t.Fatalf("nvme = %+v, want {Trigger:92 Clear:87}", nvme)
}
sata, ok := cfg.DiskFillByType["sata"]
if !ok {
t.Fatal("sata key not seeded")
}
if sata.Trigger != 90 || sata.Clear != 85 {
t.Fatalf("sata = %+v, want {Trigger:90 Clear:85}", sata)
}
hdd, ok := cfg.DiskFillByType["hdd"]
if !ok {
t.Fatal("hdd key not seeded")
}
if hdd.Trigger != 85 || hdd.Clear != 80 {
t.Fatalf("hdd = %+v, want {Trigger:85 Clear:80}", hdd)
}
})
t.Run("operator customized values survive", func(t *testing.T) {
cfg := &AlertConfig{
DiskFillByType: map[string]HysteresisThreshold{
"nvme": {Trigger: 95, Clear: 90},
},
}
alertconfig.NormalizeDiskFillByType(cfg)
nvme := cfg.DiskFillByType["nvme"]
if nvme.Trigger != 95 || nvme.Clear != 90 {
t.Fatalf("nvme = %+v, want operator value {Trigger:95 Clear:90}", nvme)
}
// Missing keys still seeded.
if sata, ok := cfg.DiskFillByType["sata"]; !ok || sata.Trigger != 90 || sata.Clear != 85 {
t.Fatalf("sata = %+v ok=%v, want default {Trigger:90 Clear:85}", sata, ok)
}
if hdd, ok := cfg.DiskFillByType["hdd"]; !ok || hdd.Trigger != 85 || hdd.Clear != 80 {
t.Fatalf("hdd = %+v ok=%v, want default {Trigger:85 Clear:80}", hdd, ok)
}
})
t.Run("negative trigger resets to default", func(t *testing.T) {
cfg := &AlertConfig{
DiskFillByType: map[string]HysteresisThreshold{
"nvme": {Trigger: -1, Clear: 10},
"sata": {Trigger: 88, Clear: -5},
},
}
alertconfig.NormalizeDiskFillByType(cfg)
nvme := cfg.DiskFillByType["nvme"]
if nvme.Trigger != 92 || nvme.Clear != 87 {
t.Fatalf("nvme = %+v, want default reset {Trigger:92 Clear:87}", nvme)
}
sata := cfg.DiskFillByType["sata"]
if sata.Trigger != 90 || sata.Clear != 85 {
t.Fatalf("sata = %+v, want default reset {Trigger:90 Clear:85}", sata)
}
})
t.Run("mixed case keys lowercase to canonical", func(t *testing.T) {
cfg := &AlertConfig{
DiskFillByType: map[string]HysteresisThreshold{
"NVMe": {Trigger: 93, Clear: 88},
},
}
alertconfig.NormalizeDiskFillByType(cfg)
if _, exists := cfg.DiskFillByType["NVMe"]; exists {
t.Fatalf("expected mixed-case key NVMe to be removed, map=%+v", cfg.DiskFillByType)
}
nvme, ok := cfg.DiskFillByType["nvme"]
if !ok {
t.Fatalf("expected lowercase nvme key, map=%+v", cfg.DiskFillByType)
}
if nvme.Trigger != 93 || nvme.Clear != 88 {
t.Fatalf("nvme = %+v, want preserved operator value {Trigger:93 Clear:88}", nvme)
}
})
}
func TestDefaultAlertConfigSeedsDiskFillByType(t *testing.T) {
cfg := defaultAlertConfig()
if cfg.DiskFillByType == nil {
t.Fatal("expected defaultAlertConfig to seed DiskFillByType, got nil")
}
if nvme, ok := cfg.DiskFillByType["nvme"]; !ok || nvme.Trigger != 92 || nvme.Clear != 87 {
t.Fatalf("nvme = %+v ok=%v, want {Trigger:92 Clear:87}", nvme, ok)
}
if sata, ok := cfg.DiskFillByType["sata"]; !ok || sata.Trigger != 90 || sata.Clear != 85 {
t.Fatalf("sata = %+v ok=%v, want {Trigger:90 Clear:85}", sata, ok)
}
if hdd, ok := cfg.DiskFillByType["hdd"]; !ok || hdd.Trigger != 85 || hdd.Clear != 80 {
t.Fatalf("hdd = %+v ok=%v, want {Trigger:85 Clear:80}", hdd, ok)
}
}