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:
2026-07-29 10:30:54 +03:00
co-authored by Claude Sonnet 5
parent f9d328b5cf
commit 586ac782b2
5 changed files with 235 additions and 3 deletions
+101 -3
View File
@@ -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