When the BMC HDD API returns an empty array (RAID controller attached via PCIe, e.g. PM8204-2GB), disk serial numbers are now recovered from smartd startup messages in SOLHostCapture.log. Enrichment runs in three passes: model-match on existing slots, positional fill of empty backplane placeholders, then new entries for any remainder. Both log/ and runningdata/var/ copies are merged with serial deduplication. Parser version bumped to 2.1. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
248 lines
6.7 KiB
Go
248 lines
6.7 KiB
Go
package inspur
|
|
|
|
import (
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
"git.mchus.pro/mchus/logpile/internal/parser"
|
|
)
|
|
|
|
// solSmartdDeviceRe matches smartd device info lines from SOLHostCapture.log.
|
|
// Example:
|
|
//
|
|
// Device: /dev/sda [SAT], Micron_5400_MTFDDAK480TGA, S/N:2310400DC7E3, WWN:..., FW:D4CM003, 480 GB
|
|
var solSmartdDeviceRe = regexp.MustCompile(
|
|
`Device: /dev/\S+ \[SAT\], (.+?), S/N:(\S+),.*?FW:(\S+), ([\d.]+) (GB|TB)`,
|
|
)
|
|
|
|
type solSmartdDevice struct {
|
|
Model string
|
|
Serial string
|
|
Firmware string
|
|
SizeGB int
|
|
}
|
|
|
|
// parseSOLSmartdDevices extracts unique disk entries from SOLHostCapture.log content.
|
|
// Deduplicates by serial number (case-insensitive); preserves first-seen order.
|
|
func parseSOLSmartdDevices(content []byte) []solSmartdDevice {
|
|
seen := make(map[string]struct{})
|
|
var out []solSmartdDevice
|
|
|
|
for _, line := range strings.Split(string(content), "\n") {
|
|
m := solSmartdDeviceRe.FindStringSubmatch(line)
|
|
if m == nil {
|
|
continue
|
|
}
|
|
serial := strings.TrimSpace(m[2])
|
|
if serial == "" {
|
|
continue
|
|
}
|
|
key := strings.ToLower(serial)
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
|
|
sizeGB := parseSolSizeGB(m[4], m[5])
|
|
out = append(out, solSmartdDevice{
|
|
Model: strings.TrimSpace(m[1]),
|
|
Serial: serial,
|
|
Firmware: strings.TrimSpace(m[3]),
|
|
SizeGB: sizeGB,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// parseSolSizeGB converts smartd size string ("480", "3.84") + unit ("GB", "TB") to integer GB.
|
|
// Uses decimal TB (1 TB = 1000 GB) matching disk manufacturer conventions.
|
|
func parseSolSizeGB(value, unit string) int {
|
|
f, err := strconv.ParseFloat(value, 64)
|
|
if err != nil || f <= 0 {
|
|
return 0
|
|
}
|
|
if strings.EqualFold(unit, "TB") {
|
|
f *= 1000
|
|
}
|
|
return int(f + 0.5)
|
|
}
|
|
|
|
// enrichStorageFromSOLSmartd enriches the storage inventory using disk info from
|
|
// SOLHostCapture.log (smartd startup messages). Both the log/ and runningdata/ copies
|
|
// are processed; serials are deduplicated across both files.
|
|
//
|
|
// Enrichment priority:
|
|
// 1. Exact model match to existing entries that are missing a serial.
|
|
// 2. Positional assignment to present placeholder slots (no model, no serial).
|
|
// 3. New entries added for any remaining devices.
|
|
func enrichStorageFromSOLSmartd(files []parser.ExtractedFile, hw *models.HardwareConfig) {
|
|
if hw == nil {
|
|
return
|
|
}
|
|
|
|
solFiles := parser.FindFileByPattern(files, "SOLHostCapture.log")
|
|
if len(solFiles) == 0 {
|
|
return
|
|
}
|
|
|
|
// Collect unique devices from all SOL log copies.
|
|
seenSerial := make(map[string]struct{})
|
|
var devices []solSmartdDevice
|
|
for _, f := range solFiles {
|
|
for _, d := range parseSOLSmartdDevices(f.Content) {
|
|
key := strings.ToLower(d.Serial)
|
|
if _, ok := seenSerial[key]; ok {
|
|
continue
|
|
}
|
|
seenSerial[key] = struct{}{}
|
|
devices = append(devices, d)
|
|
}
|
|
}
|
|
if len(devices) == 0 {
|
|
return
|
|
}
|
|
|
|
// Skip devices whose serial already appears in the storage inventory.
|
|
existingSerials := make(map[string]struct{}, len(hw.Storage))
|
|
for _, dev := range hw.Storage {
|
|
sn := strings.ToLower(strings.TrimSpace(dev.SerialNumber))
|
|
if sn != "" {
|
|
existingSerials[sn] = struct{}{}
|
|
}
|
|
}
|
|
var newDevices []solSmartdDevice
|
|
for _, d := range devices {
|
|
if _, ok := existingSerials[strings.ToLower(d.Serial)]; !ok {
|
|
newDevices = append(newDevices, d)
|
|
}
|
|
}
|
|
if len(newDevices) == 0 {
|
|
return
|
|
}
|
|
|
|
// Pass 1: enrich existing entries that match by model (first-match wins per device).
|
|
remaining := solEnrichByModel(hw, newDevices)
|
|
if len(remaining) == 0 {
|
|
return
|
|
}
|
|
|
|
// Pass 2: assign to present placeholder slots (present=true, no model, no serial).
|
|
remaining = solEnrichByPlaceholder(hw, remaining)
|
|
if len(remaining) == 0 {
|
|
return
|
|
}
|
|
|
|
// Pass 3: add as new storage entries without a slot assignment.
|
|
for _, d := range remaining {
|
|
hw.Storage = append(hw.Storage, solMakeStorage(d))
|
|
}
|
|
}
|
|
|
|
// solEnrichByModel fills SerialNumber (and optionally Firmware/SizeGB) on existing storage
|
|
// entries whose model matches the smartd model exactly. Returns unmatched devices.
|
|
func solEnrichByModel(hw *models.HardwareConfig, devices []solSmartdDevice) []solSmartdDevice {
|
|
var unmatched []solSmartdDevice
|
|
for _, d := range devices {
|
|
matched := false
|
|
for i := range hw.Storage {
|
|
if strings.TrimSpace(hw.Storage[i].SerialNumber) != "" {
|
|
continue
|
|
}
|
|
if !strings.EqualFold(strings.TrimSpace(hw.Storage[i].Model), d.Model) {
|
|
continue
|
|
}
|
|
hw.Storage[i].SerialNumber = d.Serial
|
|
if strings.TrimSpace(hw.Storage[i].Firmware) == "" {
|
|
hw.Storage[i].Firmware = d.Firmware
|
|
}
|
|
if hw.Storage[i].SizeGB == 0 {
|
|
hw.Storage[i].SizeGB = d.SizeGB
|
|
}
|
|
matched = true
|
|
break
|
|
}
|
|
if !matched {
|
|
unmatched = append(unmatched, d)
|
|
}
|
|
}
|
|
return unmatched
|
|
}
|
|
|
|
// solEnrichByPlaceholder assigns smartd devices to present storage entries that have
|
|
// neither a model nor a serial number, sorted by slot name. Returns unmatched devices.
|
|
func solEnrichByPlaceholder(hw *models.HardwareConfig, devices []solSmartdDevice) []solSmartdDevice {
|
|
type slot struct {
|
|
index int
|
|
name string
|
|
}
|
|
var placeholders []slot
|
|
for i := range hw.Storage {
|
|
if !hw.Storage[i].Present {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(hw.Storage[i].SerialNumber) != "" {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(hw.Storage[i].Model) != "" {
|
|
continue
|
|
}
|
|
placeholders = append(placeholders, slot{index: i, name: hw.Storage[i].Slot})
|
|
}
|
|
sort.Slice(placeholders, func(i, j int) bool {
|
|
return placeholders[i].name < placeholders[j].name
|
|
})
|
|
|
|
pi := 0
|
|
var unmatched []solSmartdDevice
|
|
for _, d := range devices {
|
|
if pi >= len(placeholders) {
|
|
unmatched = append(unmatched, d)
|
|
continue
|
|
}
|
|
idx := placeholders[pi].index
|
|
pi++
|
|
hw.Storage[idx].SerialNumber = d.Serial
|
|
hw.Storage[idx].Model = d.Model
|
|
hw.Storage[idx].Firmware = d.Firmware
|
|
if hw.Storage[idx].SizeGB == 0 {
|
|
hw.Storage[idx].SizeGB = d.SizeGB
|
|
}
|
|
hw.Storage[idx].Type = solStorageType(d.Model)
|
|
if hw.Storage[idx].Manufacturer == "" {
|
|
hw.Storage[idx].Manufacturer = extractStorageManufacturer(d.Model)
|
|
}
|
|
if hw.Storage[idx].Interface == "" {
|
|
hw.Storage[idx].Interface = "SATA"
|
|
}
|
|
}
|
|
return unmatched
|
|
}
|
|
|
|
func solMakeStorage(d solSmartdDevice) models.Storage {
|
|
return models.Storage{
|
|
Model: d.Model,
|
|
SerialNumber: d.Serial,
|
|
Firmware: d.Firmware,
|
|
SizeGB: d.SizeGB,
|
|
Type: solStorageType(d.Model),
|
|
Manufacturer: extractStorageManufacturer(d.Model),
|
|
Interface: "SATA",
|
|
Present: true,
|
|
}
|
|
}
|
|
|
|
// solStorageType infers SSD vs HDD from the model string.
|
|
// Micron SSD models start with "MTFDD"; Intel SSDs contain "SSD".
|
|
func solStorageType(model string) string {
|
|
upper := strings.ToUpper(model)
|
|
if strings.Contains(upper, "SSD") ||
|
|
strings.HasPrefix(upper, "MTFDD") ||
|
|
strings.HasPrefix(upper, "MICRON_5") {
|
|
return "SSD"
|
|
}
|
|
return "HDD"
|
|
}
|