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>
95 lines
2.8 KiB
Go
95 lines
2.8 KiB
Go
package inspur
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// auditSNChangedNVMeRegex matches:
|
|
// "Front Back Plane N NVMe DiskM SN changed from X to Y"
|
|
// Captures: disk_num, new_serial
|
|
var auditSNChangedNVMeRegex = regexp.MustCompile(`NVMe Disk(\d+)\s+SN changed from \S+\s+to\s+(\S+)`)
|
|
|
|
// auditSNChangedRAIDRegex matches:
|
|
// "Raid(Pcie Slot:N) HDD(enclosure id:E slot:S) SN changed from X to Y"
|
|
// Captures: pcie_slot, enclosure_id, slot_num, new_serial
|
|
var auditSNChangedRAIDRegex = regexp.MustCompile(`Raid\(Pcie Slot:(\d+)\) HDD\(enclosure id:(\d+) slot:(\d+)\)\s+SN changed from \S+\s+to\s+(\S+)`)
|
|
|
|
// ParseAuditLogNVMeSerials parses audit.log and returns the final (latest) serial number
|
|
// per NVMe disk number. The disk number matches the numeric suffix in PCIe location
|
|
// strings like "#NVME0", "#NVME2", etc. from devicefrusdr.log.
|
|
// Entries where the serial changed to "NULL" are excluded.
|
|
func ParseAuditLogNVMeSerials(content []byte) map[int]string {
|
|
serials := make(map[int]string)
|
|
|
|
for _, line := range strings.Split(string(content), "\n") {
|
|
m := auditSNChangedNVMeRegex.FindStringSubmatch(line)
|
|
if m == nil {
|
|
continue
|
|
}
|
|
diskNum, err := strconv.Atoi(m[1])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
serial := strings.TrimSpace(m[2])
|
|
if strings.EqualFold(serial, "NULL") || serial == "" {
|
|
delete(serials, diskNum)
|
|
} else {
|
|
serials[diskNum] = serial
|
|
}
|
|
}
|
|
if len(serials) == 0 {
|
|
return nil
|
|
}
|
|
return serials
|
|
}
|
|
|
|
// ParseAuditLogRAIDSerials parses audit.log and returns the final (latest) serial number
|
|
// per RAID backplane disk. Key format is "BP{enclosure_id-1}:{slot_num}" (e.g. "BP0:0").
|
|
//
|
|
// Each disk slot is claimed by a specific RAID controller (Pcie Slot:N). NULL events from
|
|
// an old controller do not clear serials assigned by a newer controller, preventing stale
|
|
// deletions when disks are migrated between RAID arrays.
|
|
func ParseAuditLogRAIDSerials(content []byte) map[string]string {
|
|
// owner tracks which PCIe RAID controller slot last assigned a serial to a disk key.
|
|
serials := make(map[string]string)
|
|
owner := make(map[string]int)
|
|
|
|
for _, line := range strings.Split(string(content), "\n") {
|
|
m := auditSNChangedRAIDRegex.FindStringSubmatch(line)
|
|
if m == nil {
|
|
continue
|
|
}
|
|
pcieSlot, err := strconv.Atoi(m[1])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
enclosureID, err := strconv.Atoi(m[2])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
slotNum, err := strconv.Atoi(m[3])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
serial := strings.TrimSpace(m[4])
|
|
key := fmt.Sprintf("BP%d:%d", enclosureID-1, slotNum)
|
|
if strings.EqualFold(serial, "NULL") || serial == "" {
|
|
// Only clear if this controller was the last to set the serial.
|
|
if owner[key] == pcieSlot {
|
|
delete(serials, key)
|
|
delete(owner, key)
|
|
}
|
|
} else {
|
|
serials[key] = serial
|
|
owner[key] = pcieSlot
|
|
}
|
|
}
|
|
if len(serials) == 0 {
|
|
return nil
|
|
}
|
|
return serials
|
|
}
|