package collector import ( "bee/audit/internal/schema" "encoding/json" "log/slog" "strconv" "strings" ) type raidControllerTelemetry struct { BatteryChargePct *float64 BatteryHealthPct *float64 BatteryTemperatureC *float64 BatteryVoltageV *float64 BatteryReplaceRequired *bool ErrorDescription *string } func enrichPCIeWithRAIDTelemetry(devs []schema.HardwarePCIeDevice) []schema.HardwarePCIeDevice { byVendor := collectRAIDControllerTelemetry() if len(byVendor) == 0 { return devs } positions := map[int]int{} for i := range devs { if devs[i].VendorID == nil || !isLikelyRAIDController(devs[i]) { continue } vendor := *devs[i].VendorID list := byVendor[vendor] if len(list) == 0 { continue } index := positions[vendor] if index >= len(list) { continue } positions[vendor] = index + 1 applyRAIDControllerTelemetry(&devs[i], list[index]) } return devs } func applyRAIDControllerTelemetry(dev *schema.HardwarePCIeDevice, tel raidControllerTelemetry) { if tel.BatteryChargePct != nil { dev.BatteryChargePct = tel.BatteryChargePct } if tel.BatteryHealthPct != nil { dev.BatteryHealthPct = tel.BatteryHealthPct } if tel.BatteryTemperatureC != nil { dev.BatteryTemperatureC = tel.BatteryTemperatureC } if tel.BatteryVoltageV != nil { dev.BatteryVoltageV = tel.BatteryVoltageV } if tel.BatteryReplaceRequired != nil { dev.BatteryReplaceRequired = tel.BatteryReplaceRequired } if tel.ErrorDescription != nil { dev.ErrorDescription = tel.ErrorDescription if dev.Status == nil || *dev.Status == statusOK { status := statusWarning dev.Status = &status } } } func collectRAIDControllerTelemetry() map[int][]raidControllerTelemetry { out := map[int][]raidControllerTelemetry{} if raw, err := raidToolQuery("storcli64", "/call", "show", "all", "J"); err == nil { list := parseStorcliControllerTelemetry(raw) if len(list) > 0 { out[vendorBroadcomLSI] = append(out[vendorBroadcomLSI], list...) slog.Info("raid: storcli controller telemetry", "count", len(list)) } } if raw, err := raidToolQuery("ssacli", "ctrl", "all", "show", "config", "detail"); err == nil { list := parseSSACLIControllerTelemetry(string(raw)) if len(list) > 0 { out[vendorHPE] = append(out[vendorHPE], list...) slog.Info("raid: ssacli controller telemetry", "count", len(list)) } } if raw, err := raidToolQuery("arcconf", "getconfig", "1", "ad"); err == nil { list := parseArcconfControllerTelemetry(string(raw)) if len(list) > 0 { out[vendorAdaptec] = append(out[vendorAdaptec], list...) slog.Info("raid: arcconf controller telemetry", "count", len(list)) } } return out } func parseStorcliControllerTelemetry(raw []byte) []raidControllerTelemetry { var doc struct { Controllers []struct { ResponseData map[string]any `json:"Response Data"` } `json:"Controllers"` } if err := json.Unmarshal(raw, &doc); err != nil { slog.Warn("raid: parse storcli controller telemetry failed", "err", err) return nil } var out []raidControllerTelemetry for _, ctl := range doc.Controllers { tel := raidControllerTelemetry{} mergeStorcliBatteryMap(&tel, nestedStringMap(ctl.ResponseData["BBU_Info"])) mergeStorcliBatteryMap(&tel, nestedStringMap(ctl.ResponseData["BBU_Info_Details"])) mergeStorcliBatteryMap(&tel, nestedStringMap(ctl.ResponseData["CV_Info"])) mergeStorcliBatteryMap(&tel, nestedStringMap(ctl.ResponseData["CV_Info_Details"])) if hasRAIDControllerTelemetry(tel) { out = append(out, tel) } } return out } func nestedStringMap(raw any) map[string]string { switch value := raw.(type) { case map[string]any: out := map[string]string{} flattenStringMap("", value, out) return out case []any: out := map[string]string{} for _, item := range value { if m, ok := item.(map[string]any); ok { flattenStringMap("", m, out) } } return out default: return nil } } func flattenStringMap(prefix string, in map[string]any, out map[string]string) { for key, raw := range in { fullKey := strings.TrimSpace(strings.ToLower(strings.Trim(prefix+" "+key, " "))) switch value := raw.(type) { case map[string]any: flattenStringMap(fullKey, value, out) case []any: for _, item := range value { if m, ok := item.(map[string]any); ok { flattenStringMap(fullKey, m, out) } } case string: out[fullKey] = value case json.Number: out[fullKey] = value.String() case float64: out[fullKey] = strconv.FormatFloat(value, 'f', -1, 64) case bool: if value { out[fullKey] = "true" } else { out[fullKey] = "false" } } } } func mergeStorcliBatteryMap(tel *raidControllerTelemetry, fields map[string]string) { if len(fields) == 0 { return } for key, raw := range fields { lower := strings.ToLower(strings.TrimSpace(key)) switch { case strings.Contains(lower, "relative state of charge"), strings.Contains(lower, "remaining capacity"), strings.Contains(lower, "charge"): if tel.BatteryChargePct == nil { tel.BatteryChargePct = parsePercentPtr(raw) } case strings.Contains(lower, "state of health"), strings.Contains(lower, "health"): if tel.BatteryHealthPct == nil { tel.BatteryHealthPct = parsePercentPtr(raw) } case strings.Contains(lower, "temperature"): if tel.BatteryTemperatureC == nil { tel.BatteryTemperatureC = parseFloatPtr(raw) } case strings.Contains(lower, "voltage"): if tel.BatteryVoltageV == nil { tel.BatteryVoltageV = parseFloatPtr(raw) } case strings.Contains(lower, "replace"), strings.Contains(lower, "replacement required"): if tel.BatteryReplaceRequired == nil { tel.BatteryReplaceRequired = parseReplaceRequired(raw) } case strings.Contains(lower, "learn cycle requested"), strings.Contains(lower, "battery state"), strings.Contains(lower, "capacitance state"): if desc := batteryStateDescription(raw); desc != nil && tel.ErrorDescription == nil { tel.ErrorDescription = desc } } } } func parseSSACLIControllerTelemetry(raw string) []raidControllerTelemetry { lines := strings.Split(raw, "\n") var out []raidControllerTelemetry var current *raidControllerTelemetry flush := func() { if current != nil && hasRAIDControllerTelemetry(*current) { out = append(out, *current) } current = nil } for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed == "" { continue } if strings.HasPrefix(strings.ToLower(trimmed), "smart array") || strings.HasPrefix(strings.ToLower(trimmed), "controller ") { flush() current = &raidControllerTelemetry{} continue } if current == nil { continue } if idx := strings.Index(trimmed, ":"); idx > 0 { key := strings.ToLower(strings.TrimSpace(trimmed[:idx])) val := strings.TrimSpace(trimmed[idx+1:]) switch { case strings.Contains(key, "capacitor temperature"), strings.Contains(key, "battery temperature"): current.BatteryTemperatureC = parseFloatPtr(val) case strings.Contains(key, "capacitor voltage"), strings.Contains(key, "battery voltage"): current.BatteryVoltageV = parseFloatPtr(val) case strings.Contains(key, "capacitor charge"), strings.Contains(key, "battery charge"): current.BatteryChargePct = parsePercentPtr(val) case strings.Contains(key, "capacitor health"), strings.Contains(key, "battery health"): current.BatteryHealthPct = parsePercentPtr(val) case strings.Contains(key, "replace") || strings.Contains(key, "failed"): if current.BatteryReplaceRequired == nil { current.BatteryReplaceRequired = parseReplaceRequired(val) } if desc := batteryStateDescription(val); desc != nil && current.ErrorDescription == nil { current.ErrorDescription = desc } } } } flush() return out } func parseArcconfControllerTelemetry(raw string) []raidControllerTelemetry { lines := strings.Split(raw, "\n") tel := raidControllerTelemetry{} for _, line := range lines { trimmed := strings.TrimSpace(line) if idx := strings.Index(trimmed, ":"); idx > 0 { key := strings.ToLower(strings.TrimSpace(trimmed[:idx])) val := strings.TrimSpace(trimmed[idx+1:]) switch { case strings.Contains(key, "battery temperature"), strings.Contains(key, "capacitor temperature"): tel.BatteryTemperatureC = parseFloatPtr(val) case strings.Contains(key, "battery voltage"), strings.Contains(key, "capacitor voltage"): tel.BatteryVoltageV = parseFloatPtr(val) case strings.Contains(key, "battery charge"), strings.Contains(key, "capacitor charge"): tel.BatteryChargePct = parsePercentPtr(val) case strings.Contains(key, "battery health"), strings.Contains(key, "capacitor health"): tel.BatteryHealthPct = parsePercentPtr(val) case strings.Contains(key, "replace"), strings.Contains(key, "failed"): if tel.BatteryReplaceRequired == nil { tel.BatteryReplaceRequired = parseReplaceRequired(val) } if desc := batteryStateDescription(val); desc != nil && tel.ErrorDescription == nil { tel.ErrorDescription = desc } } } } if hasRAIDControllerTelemetry(tel) { return []raidControllerTelemetry{tel} } return nil } func hasRAIDControllerTelemetry(tel raidControllerTelemetry) bool { return tel.BatteryChargePct != nil || tel.BatteryHealthPct != nil || tel.BatteryTemperatureC != nil || tel.BatteryVoltageV != nil || tel.BatteryReplaceRequired != nil || tel.ErrorDescription != nil } func parsePercentPtr(raw string) *float64 { raw = strings.ReplaceAll(strings.TrimSpace(raw), "%", "") return parseFloatPtr(raw) } func parseReplaceRequired(raw string) *bool { lower := strings.ToLower(strings.TrimSpace(raw)) switch { case lower == "": return nil case strings.Contains(lower, "replace"), strings.Contains(lower, "failed"), strings.Contains(lower, "yes"), strings.Contains(lower, "required"): value := true return &value case strings.Contains(lower, "no"), strings.Contains(lower, "ok"), strings.Contains(lower, "good"), strings.Contains(lower, "optimal"): value := false return &value default: return nil } } func batteryStateDescription(raw string) *string { lower := strings.ToLower(strings.TrimSpace(raw)) if lower == "" { return nil } switch { case strings.Contains(lower, "failed"), strings.Contains(lower, "fault"), strings.Contains(lower, "replace"), strings.Contains(lower, "warning"), strings.Contains(lower, "degraded"): return &raw default: return nil } }