Supermicro HGX exposes each GPU under both Chassis/1/PCIeDevices and a dedicated Chassis/HGX_GPU_SXM_N/PCIeDevices. gpuDocDedupKey was keying by @odata.id path, so identical GPUs with the same serial were not deduplicated across sources. Now stable identifiers (serial → BDF → slot+model) take priority over path. Also includes Inspur parser improvements: NVMe model/serial enrichment from devicefrusdr.log and audit.log, RAID drive slot normalization to BP notation, PSU slot normalization, BMC/CPLD/VR firmware from RESTful version info section, and parser version bump to 1.8. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
167 lines
3.6 KiB
Go
167 lines
3.6 KiB
Go
package inspur
|
|
|
|
import (
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
"git.mchus.pro/mchus/logpile/internal/parser"
|
|
)
|
|
|
|
var bpHDDSerialTokenRegex = regexp.MustCompile(`[A-Za-z0-9]{8,32}`)
|
|
|
|
func enrichStorageFromSerialFallbackFiles(files []parser.ExtractedFile, hw *models.HardwareConfig) {
|
|
if hw == nil {
|
|
return
|
|
}
|
|
f := parser.FindFileByName(files, "BpHDDSerialNumber.info")
|
|
if f == nil {
|
|
return
|
|
}
|
|
serials := extractBPHDDSerials(f.Content)
|
|
if len(serials) == 0 {
|
|
return
|
|
}
|
|
applyStorageSerialFallback(hw, serials)
|
|
}
|
|
|
|
func extractBPHDDSerials(content []byte) []string {
|
|
if len(content) == 0 {
|
|
return nil
|
|
}
|
|
matches := bpHDDSerialTokenRegex.FindAllString(string(content), -1)
|
|
if len(matches) == 0 {
|
|
return nil
|
|
}
|
|
|
|
out := make([]string, 0, len(matches))
|
|
seen := make(map[string]struct{}, len(matches))
|
|
for _, m := range matches {
|
|
v := normalizeRedisValue(m)
|
|
if !looksLikeStorageSerial(v) {
|
|
continue
|
|
}
|
|
key := strings.ToLower(v)
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
out = append(out, v)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func looksLikeStorageSerial(v string) bool {
|
|
if len(v) < 8 {
|
|
return false
|
|
}
|
|
hasLetter := false
|
|
hasDigit := false
|
|
for _, r := range v {
|
|
switch {
|
|
case r >= 'A' && r <= 'Z':
|
|
hasLetter = true
|
|
case r >= 'a' && r <= 'z':
|
|
hasLetter = true
|
|
case r >= '0' && r <= '9':
|
|
hasDigit = true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return hasLetter && hasDigit
|
|
}
|
|
|
|
// applyRAIDSlotSerials updates storage serial numbers using the slot→serial map
|
|
// derived from audit.log RAID SN change events. Overwrites existing serials since
|
|
// audit.log represents the authoritative current state after all disk replacements.
|
|
func applyRAIDSlotSerials(hw *models.HardwareConfig, serials map[string]string) {
|
|
if hw == nil || len(serials) == 0 {
|
|
return
|
|
}
|
|
for i := range hw.Storage {
|
|
slot := strings.TrimSpace(hw.Storage[i].Slot)
|
|
if slot == "" {
|
|
continue
|
|
}
|
|
if sn, ok := serials[slot]; ok && sn != "" {
|
|
hw.Storage[i].SerialNumber = sn
|
|
}
|
|
}
|
|
}
|
|
|
|
func applyStorageSerialFallback(hw *models.HardwareConfig, serials []string) {
|
|
if hw == nil || len(hw.Storage) == 0 || len(serials) == 0 {
|
|
return
|
|
}
|
|
|
|
existing := make(map[string]struct{}, len(hw.Storage))
|
|
for _, dev := range hw.Storage {
|
|
if sn := normalizeRedisValue(dev.SerialNumber); sn != "" {
|
|
existing[strings.ToLower(sn)] = struct{}{}
|
|
}
|
|
}
|
|
|
|
filtered := make([]string, 0, len(serials))
|
|
for _, sn := range serials {
|
|
key := strings.ToLower(sn)
|
|
if _, ok := existing[key]; ok {
|
|
continue
|
|
}
|
|
filtered = append(filtered, sn)
|
|
}
|
|
if len(filtered) == 0 {
|
|
return
|
|
}
|
|
|
|
type target struct {
|
|
index int
|
|
rank int
|
|
slot string
|
|
}
|
|
targets := make([]target, 0, len(hw.Storage))
|
|
for i := range hw.Storage {
|
|
dev := hw.Storage[i]
|
|
if normalizeRedisValue(dev.SerialNumber) != "" {
|
|
continue
|
|
}
|
|
if !dev.Present && strings.TrimSpace(dev.Slot) == "" {
|
|
continue
|
|
}
|
|
rank := 0
|
|
if !dev.Present {
|
|
rank += 10
|
|
}
|
|
if strings.EqualFold(strings.TrimSpace(dev.Type), "NVMe") {
|
|
rank += 5
|
|
}
|
|
if strings.TrimSpace(dev.Slot) == "" {
|
|
rank += 4
|
|
}
|
|
targets = append(targets, target{
|
|
index: i,
|
|
rank: rank,
|
|
slot: strings.ToLower(strings.TrimSpace(dev.Slot)),
|
|
})
|
|
}
|
|
if len(targets) == 0 {
|
|
return
|
|
}
|
|
|
|
sort.Slice(targets, func(i, j int) bool {
|
|
if targets[i].rank != targets[j].rank {
|
|
return targets[i].rank < targets[j].rank
|
|
}
|
|
return targets[i].slot < targets[j].slot
|
|
})
|
|
|
|
for i := 0; i < len(targets) && i < len(filtered); i++ {
|
|
dev := &hw.Storage[targets[i].index]
|
|
dev.SerialNumber = filtered[i]
|
|
if !dev.Present {
|
|
dev.Present = true
|
|
}
|
|
}
|
|
}
|