package collector import ( "bee/audit/internal/schema" "encoding/json" "log/slog" "os" "os/exec" "regexp" "sort" "strconv" "strings" ) const ( vendorBroadcomLSI = 0x1000 vendorAdaptec = 0x9005 vendorHPE = 0x103c vendorIntel = 0x8086 ) var raidToolQuery = func(name string, args ...string) ([]byte, error) { return exec.Command(name, args...).Output() } var readMDStat = func() ([]byte, error) { return os.ReadFile("/proc/mdstat") } // collectRAIDStorage collects physical disks behind RAID controllers that may // not be exposed as regular block devices. func collectRAIDStorage(pcie []schema.HardwarePCIeDevice) []schema.HardwareStorage { vendors := detectRAIDVendors(pcie) if len(vendors) == 0 { return nil } var out []schema.HardwareStorage if vendors[vendorBroadcomLSI] { if drives := collectStorcliDrives(); len(drives) > 0 { out = append(out, drives...) } if drives := collectSASIrcuDrives("sas3ircu"); len(drives) > 0 { out = append(out, drives...) } if drives := collectSASIrcuDrives("sas2ircu"); len(drives) > 0 { out = append(out, drives...) } } if vendors[vendorAdaptec] { if drives := collectArcconfDrives(); len(drives) > 0 { out = append(out, drives...) } } if vendors[vendorHPE] { if drives := collectSSACLIDrives(); len(drives) > 0 { out = append(out, drives...) } } if len(out) > 0 { slog.Info("raid: collected physical drives", "count", len(out)) } return out } func detectRAIDVendors(pcie []schema.HardwarePCIeDevice) map[int]bool { out := map[int]bool{} for _, dev := range pcie { if dev.VendorID == nil { continue } if isLikelyRAIDController(dev) { out[*dev.VendorID] = true } } return out } func isLikelyRAIDController(dev schema.HardwarePCIeDevice) bool { if dev.DeviceClass == nil { return false } return isRAIDClass(*dev.DeviceClass) } func collectStorcliDrives() []schema.HardwareStorage { out, err := raidToolQuery("storcli64", "/call/eall/sall", "show", "all", "J") if err != nil { slog.Info("raid: storcli unavailable", "err", err) return nil } drives := parseStorcliDrivesJSON(out) if len(drives) == 0 { slog.Info("raid: storcli returned no drives") } return drives } func collectSASIrcuDrives(tool string) []schema.HardwareStorage { out, err := raidToolQuery(tool, "list") if err != nil { slog.Info("raid: "+tool+" unavailable", "err", err) return nil } var drives []schema.HardwareStorage for _, ctlID := range parseSASIrcuControllerIDs(string(out)) { raw, err := raidToolQuery(tool, strconv.Itoa(ctlID), "display") if err != nil { continue } drives = append(drives, parseSASIrcuDisplay(string(raw))...) } return drives } func parseSASIrcuControllerIDs(raw string) []int { lines := strings.Split(raw, "\n") idsMap := map[int]bool{} for _, line := range lines { fields := strings.Fields(strings.TrimSpace(line)) if len(fields) == 0 { continue } id, err := strconv.Atoi(fields[0]) if err != nil { continue } idsMap[id] = true } var ids []int for id := range idsMap { ids = append(ids, id) } sort.Ints(ids) return ids } func parseSASIrcuDisplay(raw string) []schema.HardwareStorage { var blocks []map[string]string var cur map[string]string var currentType string for _, line := range strings.Split(raw, "\n") { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "Device is a ") { if cur != nil { cur["__device_type"] = currentType blocks = append(blocks, cur) } cur = map[string]string{} currentType = strings.TrimSpace(strings.TrimPrefix(trimmed, "Device is a ")) continue } if cur == nil { continue } if idx := strings.Index(trimmed, ":"); idx > 0 { key := strings.TrimSpace(trimmed[:idx]) val := strings.TrimSpace(trimmed[idx+1:]) cur[key] = val } } if cur != nil { cur["__device_type"] = currentType blocks = append(blocks, cur) } var out []schema.HardwareStorage for _, b := range blocks { dt := strings.ToLower(b["__device_type"]) if !strings.Contains(dt, "hard disk") && !strings.Contains(dt, "ssd") && !strings.Contains(dt, "nvme") { continue } present := true status := mapRAIDDriveStatus(b["State"]) s := schema.HardwareStorage{ HardwareComponentStatus: schema.HardwareComponentStatus{Status: &status}, Present: &present, } enclosure := strings.TrimSpace(b["Enclosure #"]) slot := strings.TrimSpace(b["Slot #"]) if enclosure != "" || slot != "" { v := enclosure + ":" + slot v = strings.Trim(v, ":") s.Slot = &v } if v := strings.TrimSpace(b["Model Number"]); v != "" { s.Model = &v } if v := strings.TrimSpace(b["Serial No"]); v != "" { s.SerialNumber = &v } if v := strings.ToUpper(strings.TrimSpace(b["Protocol"])); v != "" { s.Interface = &v } media := strings.ToUpper(strings.TrimSpace(b["Drive Type"])) if media == "" { media = strings.ToUpper(dt) } intf := "" if s.Interface != nil { intf = *s.Interface } devType := inferDriveType(media, intf) s.Type = &devType if mb := parseSASIrcuMB(b["Size (in MB)/(in sectors)"]); mb > 0 { gb := mb / 1000 if gb == 0 { gb = 1 } s.SizeGB = &gb } if s.Slot != nil || s.SerialNumber != nil || s.Model != nil { out = append(out, s) } } return out } func parseSASIrcuMB(raw string) int { raw = strings.TrimSpace(raw) if raw == "" { return 0 } head := strings.SplitN(raw, "/", 2)[0] n, err := strconv.Atoi(strings.TrimSpace(head)) if err != nil { return 0 } return n } func collectArcconfDrives() []schema.HardwareStorage { raw, err := raidToolQuery("arcconf", "getconfig", "1", "pd") if err != nil { slog.Info("raid: arcconf unavailable", "err", err) return nil } return parseArcconfPhysicalDrives(string(raw)) } func parseArcconfPhysicalDrives(raw string) []schema.HardwareStorage { lines := strings.Split(raw, "\n") var blocks []map[string]string var cur map[string]string for _, line := range lines { trimmed := strings.TrimSpace(line) if strings.HasPrefix(strings.ToLower(trimmed), "device #") { if cur != nil { blocks = append(blocks, cur) } cur = map[string]string{} continue } if cur == nil { continue } if idx := strings.Index(trimmed, ":"); idx > 0 { key := strings.TrimSpace(trimmed[:idx]) val := strings.TrimSpace(trimmed[idx+1:]) cur[key] = val } } if cur != nil { blocks = append(blocks, cur) } var out []schema.HardwareStorage for _, b := range blocks { present := true status := mapRAIDDriveStatus(b["State"]) s := schema.HardwareStorage{ HardwareComponentStatus: schema.HardwareComponentStatus{Status: &status}, Present: &present, } if v := strings.TrimSpace(b["Reported Location"]); v != "" { s.Slot = &v } if v := strings.TrimSpace(b["Model"]); v != "" { s.Model = &v } if v := strings.TrimSpace(b["Serial number"]); v != "" { s.SerialNumber = &v } if gb := parseHumanSizeToGB(b["Total Size"]); gb > 0 { s.SizeGB = &gb } intf := parseArcconfInterface(b["Transfer Speed"]) if intf != "" { s.Interface = &intf } media := strings.ToUpper(strings.TrimSpace(b["SSD"])) if media == "YES" || media == "TRUE" { media = "SSD" } devType := inferDriveType(media, intf) s.Type = &devType if s.Slot != nil || s.SerialNumber != nil || s.Model != nil { out = append(out, s) } } return out } func parseArcconfInterface(raw string) string { u := strings.ToUpper(raw) switch { case strings.Contains(u, "SAS"): return "SAS" case strings.Contains(u, "SATA"): return "SATA" case strings.Contains(u, "NVME"): return "NVME" default: return "" } } var ssacliPhysicalDriveLine = regexp.MustCompile(`(?i)^physicaldrive\s+(\S+)\s+\(([^)]*)\)$`) func collectSSACLIDrives() []schema.HardwareStorage { raw, err := raidToolQuery("ssacli", "ctrl", "all", "show", "config", "detail") if err != nil { slog.Info("raid: ssacli unavailable", "err", err) return nil } return parseSSACLIPhysicalDrives(string(raw)) } func parseSSACLIPhysicalDrives(raw string) []schema.HardwareStorage { lines := strings.Split(raw, "\n") var out []schema.HardwareStorage var cur *schema.HardwareStorage flush := func() { if cur == nil { return } if cur.Slot != nil || cur.SerialNumber != nil || cur.Model != nil { out = append(out, *cur) } cur = nil } for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed == "" { continue } if m := ssacliPhysicalDriveLine.FindStringSubmatch(trimmed); len(m) == 3 { flush() present := true status := statusUnknown s := schema.HardwareStorage{ HardwareComponentStatus: schema.HardwareComponentStatus{Status: &status}, Present: &present, } slot := m[1] s.Slot = &slot meta := strings.Split(m[2], ",") if len(meta) > 0 { if gb := parseHumanSizeToGB(strings.TrimSpace(meta[0])); gb > 0 { s.SizeGB = &gb } } if len(meta) > 1 { intf := parseSSACLIInterface(meta[1]) if intf != "" { s.Interface = &intf } devType := inferDriveType(strings.ToUpper(meta[1]), intf) s.Type = &devType } if len(meta) > 2 { st := mapRAIDDriveStatus(meta[len(meta)-1]) s.Status = &st } cur = &s continue } if cur == nil { continue } if idx := strings.Index(trimmed, ":"); idx > 0 { key := strings.ToLower(strings.TrimSpace(trimmed[:idx])) val := strings.TrimSpace(trimmed[idx+1:]) switch key { case "serial number": if val != "" { cur.SerialNumber = &val } case "model": if val != "" { cur.Model = &val } case "status": st := mapRAIDDriveStatus(val) cur.Status = &st } } } flush() return out } func parseSSACLIInterface(raw string) string { u := strings.ToUpper(raw) switch { case strings.Contains(u, "SAS"): return "SAS" case strings.Contains(u, "SATA"): return "SATA" case strings.Contains(u, "NVME"): return "NVME" default: return "" } } func parseStorcliDrivesJSON(raw []byte) []schema.HardwareStorage { var doc struct { Controllers []struct { ResponseData struct { DriveInformation []struct { EIDSlt string `json:"EID:Slt"` State string `json:"State"` Size string `json:"Size"` Intf string `json:"Intf"` Med string `json:"Med"` Model string `json:"Model"` SN string `json:"SN"` Sp string `json:"Sp"` Type string `json:"Type"` } `json:"Drive Information"` } `json:"Response Data"` } `json:"Controllers"` } if err := json.Unmarshal(raw, &doc); err != nil { slog.Warn("raid: parse storcli json failed", "err", err) return nil } var drives []schema.HardwareStorage for _, ctl := range doc.Controllers { for _, d := range ctl.ResponseData.DriveInformation { if s := storcliDriveToStorage(d); s != nil { drives = append(drives, *s) } } } return drives } func storcliDriveToStorage(d struct { EIDSlt string `json:"EID:Slt"` State string `json:"State"` Size string `json:"Size"` Intf string `json:"Intf"` Med string `json:"Med"` Model string `json:"Model"` SN string `json:"SN"` Sp string `json:"Sp"` Type string `json:"Type"` }) *schema.HardwareStorage { present := true status := mapRAIDDriveStatus(d.State) s := schema.HardwareStorage{ HardwareComponentStatus: schema.HardwareComponentStatus{Status: &status}, Present: &present, } if v := strings.TrimSpace(d.EIDSlt); v != "" { s.Slot = &v } if v := strings.TrimSpace(d.Model); v != "" { s.Model = &v } if v := strings.TrimSpace(d.SN); v != "" { s.SerialNumber = &v } if v := strings.TrimSpace(strings.ToUpper(d.Intf)); v != "" { s.Interface = &v } devType := inferDriveType(strings.TrimSpace(strings.ToUpper(d.Med)), strings.TrimSpace(strings.ToUpper(d.Intf))) if devType != "" { s.Type = &devType } if gb := parseHumanSizeToGB(d.Size); gb > 0 { s.SizeGB = &gb } // return only meaningful records if s.Model == nil && s.SerialNumber == nil && s.Slot == nil { return nil } return &s } func inferDriveType(med, intf string) string { switch { case strings.Contains(med, "SSD"): return "SSD" case strings.Contains(intf, "NVME"): return "NVMe" case strings.Contains(med, "HDD"): return "HDD" case strings.Contains(intf, "SAS") || strings.Contains(intf, "SATA"): return "HDD" default: return "Unknown" } } func mapRAIDDriveStatus(raw string) string { u := strings.ToUpper(strings.TrimSpace(raw)) switch { case strings.Contains(u, "OK"), strings.Contains(u, "OPTIMAL"), strings.Contains(u, "READY"): return statusOK case strings.Contains(u, "ONLN"), strings.Contains(u, "ONLINE"): return statusOK case strings.Contains(u, "RBLD"), strings.Contains(u, "REBUILD"): return statusWarning case strings.Contains(u, "FAIL"), strings.Contains(u, "OFFLINE"): return statusCritical default: return statusUnknown } } func parseHumanSizeToGB(raw string) int { parts := strings.Fields(strings.TrimSpace(raw)) if len(parts) < 2 { return 0 } value, err := strconv.ParseFloat(strings.TrimSpace(parts[0]), 64) if err != nil { return 0 } unit := strings.ToUpper(parts[1]) switch { case strings.HasPrefix(unit, "TB"): return int(value * 1000) case strings.HasPrefix(unit, "GB"): return int(value) case strings.HasPrefix(unit, "MB"): return int(value / 1000) default: return 0 } } func appendUniqueStorage(base, extra []schema.HardwareStorage) []schema.HardwareStorage { if len(extra) == 0 { return base } seen := map[string]bool{} for _, d := range base { seen[storageIdentityKey(d)] = true } for _, d := range extra { key := storageIdentityKey(d) if key == "" || seen[key] { continue } base = append(base, d) seen[key] = true } return base } func storageIdentityKey(d schema.HardwareStorage) string { if d.SerialNumber != nil && strings.TrimSpace(*d.SerialNumber) != "" { return "sn:" + strings.ToLower(strings.TrimSpace(*d.SerialNumber)) } if d.Model != nil && d.Slot != nil { return "modelslot:" + strings.ToLower(strings.TrimSpace(*d.Model)) + ":" + strings.ToLower(strings.TrimSpace(*d.Slot)) } return "" } type mdArray struct { Name string Degraded bool Members []string } func enrichStorageWithVROC(storage []schema.HardwareStorage, pcie []schema.HardwarePCIeDevice) []schema.HardwareStorage { if !hasVROCController(pcie) { return storage } raw, err := readMDStat() if err != nil { slog.Info("vroc: cannot read /proc/mdstat", "err", err) return storage } arrays := parseMDStatArrays(string(raw)) if len(arrays) == 0 { slog.Info("vroc: no md arrays found") return storage } serialToArray := map[string]mdArray{} for _, arr := range arrays { for _, member := range arr.Members { serial := queryDeviceSerial("/dev/" + member) if serial == "" { continue } serialToArray[strings.ToLower(serial)] = arr } } if len(serialToArray) == 0 { return storage } updated := 0 for i := range storage { if storage[i].SerialNumber == nil || strings.TrimSpace(*storage[i].SerialNumber) == "" { continue } arr, ok := serialToArray[strings.ToLower(strings.TrimSpace(*storage[i].SerialNumber))] if !ok { continue } if storage[i].Telemetry == nil { storage[i].Telemetry = map[string]any{} } storage[i].Telemetry["vroc_array"] = arr.Name storage[i].Telemetry["vroc_degraded"] = arr.Degraded if arr.Degraded { status := statusWarning storage[i].Status = &status storage[i].ErrorDescription = stringPtr("VROC array is degraded") } updated++ } slog.Info("vroc: enriched storage members", "count", updated) return storage } func hasVROCController(pcie []schema.HardwarePCIeDevice) bool { for _, dev := range pcie { if dev.VendorID == nil || *dev.VendorID != vendorIntel { continue } class := "" if dev.DeviceClass != nil { class = strings.TrimSpace(*dev.DeviceClass) } model := "" if dev.Model != nil { model = strings.ToLower(*dev.Model) } if isRAIDClass(class) || strings.Contains(model, "vroc") || strings.Contains(model, "volume management device") || strings.Contains(model, "vmd") { return true } } return false } var mdHealthPattern = regexp.MustCompile(`\[[U_]+\]`) func parseMDStatArrays(raw string) []mdArray { lines := strings.Split(raw, "\n") var arrays []mdArray var current *mdArray for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed == "" { continue } if strings.Contains(line, " : ") && !strings.HasPrefix(strings.TrimLeft(line, " \t"), "[") { left := strings.TrimSpace(strings.SplitN(line, " : ", 2)[0]) if strings.EqualFold(left, "Personalities") || strings.EqualFold(left, "unused devices") { continue } if current != nil { arrays = append(arrays, *current) } name := left fields := strings.Fields(strings.SplitN(line, " : ", 2)[1]) arr := mdArray{Name: name} for _, f := range fields { if i := strings.IndexByte(f, '['); i > 0 { member := strings.TrimSpace(f[:i]) if member != "" { arr.Members = append(arr.Members, member) } } } current = &arr continue } if current == nil { continue } if m := mdHealthPattern.FindString(trimmed); m != "" && strings.Contains(m, "_") { current.Degraded = true } } if current != nil { arrays = append(arrays, *current) } return arrays } func queryDeviceSerial(devPath string) string { if out, err := exec.Command("nvme", "id-ctrl", devPath, "-o", "json").Output(); err == nil { var ctrl nvmeIDCtrl if json.Unmarshal(out, &ctrl) == nil { if v := cleanDMIValue(strings.TrimSpace(ctrl.SerialNumber)); v != "" { return v } } } if out, err := exec.Command("smartctl", "-j", "-i", devPath).Output(); err == nil { var info smartctlInfo if json.Unmarshal(out, &info) == nil { if v := cleanDMIValue(strings.TrimSpace(info.SerialNumber)); v != "" { return v } } } return "" }