335 lines
10 KiB
Go
335 lines
10 KiB
Go
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
|
|
}
|
|
}
|