fix(parser): expose NVIDIA HGX tray/baseboard identity separately from vendor carrier FRU
Inspur/Kaytus onekeylog dumps only exposed the mechanical carrier FRU serial (component/fru.txt Board Product) as board identity. That serial doesn't change when the actual NVIDIA HGX baseboard (SXM+NVSwitch "delta board") is swapped, causing false "board unchanged" conclusions. Parse the real HGX tray/baseboard Model/PartNumber/SerialNumber triples from HGX_HWInfo_FWVersion.log into a new HardwareConfig.HGX field, and normalize Redfish's NA/N-A placeholders to empty across HGX identity parsing. Closes #21 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+101
-3
@@ -48,8 +48,106 @@ var (
|
||||
reIDLine = regexp.MustCompile(`"Id":\s*"([^"]+)"`)
|
||||
reVersion = regexp.MustCompile(`"Version":\s*"([^"]*)"`)
|
||||
reSlotGPU = regexp.MustCompile(`(?i)gpu\s*#?\s*(\d+)`)
|
||||
|
||||
reCurlLine = regexp.MustCompile(`(?m)^#.*curl.*$`)
|
||||
reRedfishPath = regexp.MustCompile(`(/redfish/v1/\S+)`)
|
||||
reFieldModel = regexp.MustCompile(`"Model"\s*:\s*"([^"]*)"`)
|
||||
reFieldPart = regexp.MustCompile(`"PartNumber"\s*:\s*"([^"]*)"`)
|
||||
reFieldSerial = regexp.MustCompile(`"SerialNumber"\s*:\s*"([^"]*)"`)
|
||||
)
|
||||
|
||||
// hgxValue normalizes a raw HWInfo field value, treating Redfish's "not
|
||||
// applicable" placeholders (e.g. an unpowered/absent GPU) as empty so they
|
||||
// never overwrite or masquerade as real identity data.
|
||||
func hgxValue(raw string) string {
|
||||
v := strings.TrimSpace(raw)
|
||||
switch strings.ToUpper(v) {
|
||||
case "", "NA", "N/A":
|
||||
return ""
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// splitCurlBlocks splits a HGX_HWInfo_FWVersion.log-style dump (a sequence of
|
||||
// "# curl ... <url>" comment lines each followed by that request's JSON
|
||||
// response) into per-request chunks, so fields can be attributed to the
|
||||
// Redfish path that produced them.
|
||||
func splitCurlBlocks(content []byte) []string {
|
||||
text := string(content)
|
||||
locs := reCurlLine.FindAllStringIndex(text, -1)
|
||||
if len(locs) == 0 {
|
||||
return []string{text}
|
||||
}
|
||||
|
||||
blocks := make([]string, 0, len(locs))
|
||||
for i, loc := range locs {
|
||||
end := len(text)
|
||||
if i+1 < len(locs) {
|
||||
end = locs[i+1][0]
|
||||
}
|
||||
blocks = append(blocks, text[loc[0]:end])
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
// parseHGXIdentity extracts the NVIDIA HGX tray and baseboard hardware
|
||||
// identity (Model/PartNumber/SerialNumber) from a HGX_HWInfo_FWVersion.log
|
||||
// dump. Unlike the vendor mechanical-carrier FRU, these serials change when
|
||||
// the actual HGX tray/baseboard ("delta board") is swapped.
|
||||
func parseHGXIdentity(content []byte) *models.HGXIdentity {
|
||||
if len(content) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var identity models.HGXIdentity
|
||||
found := false
|
||||
|
||||
for _, block := range splitCurlBlocks(content) {
|
||||
path := reRedfishPath.FindString(block)
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
lowerPath := strings.ToLower(path)
|
||||
|
||||
// GPU-specific and GPU-scoped paths are handled by
|
||||
// parseHGXGPUAssembly/parseHGXGPUFirmware; skip them here so a
|
||||
// per-GPU Model/PartNumber/SerialNumber triple never gets
|
||||
// misattributed to the tray or baseboard.
|
||||
if strings.Contains(lowerPath, "gpu_sxm") || strings.Contains(lowerPath, "/processors/") {
|
||||
continue
|
||||
}
|
||||
|
||||
info := models.HGXAssemblyIdentity{}
|
||||
if m := reFieldModel.FindStringSubmatch(block); m != nil {
|
||||
info.Model = hgxValue(m[1])
|
||||
}
|
||||
if m := reFieldPart.FindStringSubmatch(block); m != nil {
|
||||
info.PartNumber = hgxValue(m[1])
|
||||
}
|
||||
if m := reFieldSerial.FindStringSubmatch(block); m != nil {
|
||||
info.SerialNumber = hgxValue(m[1])
|
||||
}
|
||||
if info.Model == "" && info.PartNumber == "" && info.SerialNumber == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(lowerPath, "tray"):
|
||||
identity.Tray = info
|
||||
found = true
|
||||
case strings.Contains(lowerPath, "baseboard"):
|
||||
identity.Baseboard = info
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
return &identity
|
||||
}
|
||||
|
||||
func enrichGPUsFromHGXHWInfo(content []byte, hw *models.HardwareConfig) {
|
||||
if hw == nil || len(hw.GPUs) == 0 || len(content) == 0 {
|
||||
return
|
||||
@@ -157,9 +255,9 @@ func parseHGXGPUAssembly(content []byte) map[int]hgxGPUAssemblyInfo {
|
||||
}
|
||||
|
||||
result[sxmIdx] = hgxGPUAssemblyInfo{
|
||||
Model: strings.TrimSpace(string(m[2])),
|
||||
Part: strings.TrimSpace(string(m[3])),
|
||||
Serial: strings.TrimSpace(string(m[4])),
|
||||
Model: hgxValue(string(m[2])),
|
||||
Part: hgxValue(string(m[3])),
|
||||
Serial: hgxValue(string(m[4])),
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package inspur
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseHGXIdentity_ExtractsTrayAndBaseboard(t *testing.T) {
|
||||
content := []byte(`
|
||||
# curl -X GET http://127.0.0.1/redfish/v1/Chassis/HGX_Tray_0/Assembly
|
||||
{"Name":"HGX Tray Assembly","Model":"P6612-A04","PartNumber":"699-26612-0000-P00","SerialNumber":"TRAYSN1"}
|
||||
# curl -X GET http://127.0.0.1/redfish/v1/Systems/HGX_Baseboard_0
|
||||
{"Model":"NVIDIA HGX B200 8 GPU","PartNumber":"935-26287-00A0-000","SerialNumber":"BBSN1"}
|
||||
# curl -X GET http://127.0.0.1/redfish/v1/Systems/HGX_Baseboard_0/Processors/GPU_SXM_1
|
||||
{"Model":"B200 180GB HBM3e","PartNumber":"692-2G525-0220-501","SerialNumber":"GPUSN1"}
|
||||
`)
|
||||
|
||||
identity := parseHGXIdentity(content)
|
||||
if identity == nil {
|
||||
t.Fatal("expected non-nil identity")
|
||||
}
|
||||
if identity.Tray.Model != "P6612-A04" || identity.Tray.PartNumber != "699-26612-0000-P00" || identity.Tray.SerialNumber != "TRAYSN1" {
|
||||
t.Fatalf("unexpected tray identity: %+v", identity.Tray)
|
||||
}
|
||||
if identity.Baseboard.Model != "NVIDIA HGX B200 8 GPU" || identity.Baseboard.PartNumber != "935-26287-00A0-000" || identity.Baseboard.SerialNumber != "BBSN1" {
|
||||
t.Fatalf("unexpected baseboard identity: %+v", identity.Baseboard)
|
||||
}
|
||||
// The per-GPU Processors path must never leak into baseboard/tray identity.
|
||||
if identity.Baseboard.SerialNumber == "GPUSN1" || identity.Tray.SerialNumber == "GPUSN1" {
|
||||
t.Fatalf("GPU serial leaked into baseboard/tray identity: %+v", identity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHGXIdentity_DetectsBaseboardSwapAcrossDumps(t *testing.T) {
|
||||
dumpA := []byte(`
|
||||
# curl -X GET http://127.0.0.1/redfish/v1/Chassis/HGX_Tray_0/Assembly
|
||||
{"Name":"HGX Tray Assembly","Model":"P6612-A04","PartNumber":"699-26612-0000-P00","SerialNumber":"TRAY-1"}
|
||||
# curl -X GET http://127.0.0.1/redfish/v1/Systems/HGX_Baseboard_0
|
||||
{"Model":"NVIDIA HGX B200 8 GPU","PartNumber":"935-26287-00A0-000","SerialNumber":"BB-1"}
|
||||
`)
|
||||
dumpC := []byte(`
|
||||
# curl -X GET http://127.0.0.1/redfish/v1/Chassis/HGX_Tray_0/Assembly
|
||||
{"Name":"HGX Tray Assembly","Model":"P6612-A04","PartNumber":"699-26612-0000-P00","SerialNumber":"TRAY-2"}
|
||||
# curl -X GET http://127.0.0.1/redfish/v1/Systems/HGX_Baseboard_0
|
||||
{"Model":"NVIDIA HGX B200 8 GPU","PartNumber":"935-26287-00A0-000","SerialNumber":"BB-2"}
|
||||
`)
|
||||
|
||||
a := parseHGXIdentity(dumpA)
|
||||
c := parseHGXIdentity(dumpC)
|
||||
if a == nil || c == nil {
|
||||
t.Fatal("expected identity from both dumps")
|
||||
}
|
||||
if a.Baseboard.SerialNumber == c.Baseboard.SerialNumber {
|
||||
t.Fatal("expected baseboard serial to differ across dumps that recorded a swap")
|
||||
}
|
||||
if a.Tray.SerialNumber == c.Tray.SerialNumber {
|
||||
t.Fatal("expected tray serial to differ across dumps that recorded a swap")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHGXIdentity_TreatsNAAsAbsent(t *testing.T) {
|
||||
content := []byte(`
|
||||
# curl -X GET http://127.0.0.1/redfish/v1/Systems/HGX_Baseboard_0
|
||||
{"Model":"NA","PartNumber":"NA","SerialNumber":"NA"}
|
||||
`)
|
||||
|
||||
if identity := parseHGXIdentity(content); identity != nil {
|
||||
t.Fatalf("expected nil identity when all fields are NA, got %+v", identity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHGXIdentity_NoHGXContentReturnsNil(t *testing.T) {
|
||||
if identity := parseHGXIdentity(nil); identity != nil {
|
||||
t.Fatalf("expected nil identity for empty content, got %+v", identity)
|
||||
}
|
||||
if identity := parseHGXIdentity([]byte("no redfish paths here")); identity != nil {
|
||||
t.Fatalf("expected nil identity when no HGX paths present, got %+v", identity)
|
||||
}
|
||||
}
|
||||
+1
@@ -278,6 +278,7 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
||||
if f := parser.FindFileByName(files, "HGX_HWInfo_FWVersion.log"); f != nil && result.Hardware != nil {
|
||||
enrichGPUsFromHGXHWInfo(f.Content, result.Hardware)
|
||||
appendHGXFirmwareFromHWInfo(f.Content, result.Hardware)
|
||||
result.Hardware.HGX = parseHGXIdentity(f.Content)
|
||||
}
|
||||
|
||||
// Mark problematic GPUs from IDL errors like "BIOS miss F_GPU6".
|
||||
|
||||
Reference in New Issue
Block a user