diff --git a/audit/internal/webui/ipmi_fru.go b/audit/internal/webui/ipmi_fru.go
index 3a8b403..f99c9f6 100644
--- a/audit/internal/webui/ipmi_fru.go
+++ b/audit/internal/webui/ipmi_fru.go
@@ -37,26 +37,41 @@ var fruEditableFields = map[string]struct {
"Chassis Part Number": {"c", 0},
"Chassis Serial Number": {"c", 1},
"Chassis Serial": {"c", 1},
- "Chassis Extra": {"c", 2},
// Board — vendor doc names and ipmitool abbreviated names
- "Board Manufacturer": {"b", 0},
- "Board Mfg": {"b", 0},
- "Board Product Name": {"b", 1},
- "Board Product": {"b", 1},
+ "Board Manufacturer": {"b", 0},
+ "Board Mfg": {"b", 0},
+ "Board Product Name": {"b", 1},
+ "Board Product": {"b", 1},
"Board Serial Number": {"b", 2},
- "Board Serial": {"b", 2},
- "Board Part Number": {"b", 3},
+ "Board Serial": {"b", 2},
+ "Board Part Number": {"b", 3},
// Product — vendor doc names and ipmitool abbreviated names
- "Product Manufacturer": {"p", 0},
- "Product Name": {"p", 1},
- "Product Part Number": {"p", 2},
- "Product Version": {"p", 3},
+ "Product Manufacturer": {"p", 0},
+ "Product Name": {"p", 1},
+ "Product Part Number": {"p", 2},
+ "Product Version": {"p", 3},
"Product Serial Number": {"p", 4},
- "Product Serial": {"p", 4},
+ "Product Serial": {"p", 4},
+ "Product Asset Tag": {"p", 5},
+}
+
+// fruExtraBaseIndex gives the starting ipmitool field index for each area's
+// repeated " Extra" custom fields, per the vendor FRU field doc (Chassis
+// extra fields start at 2, Board at 5, Product at 7). ipmitool fru print
+// emits one identically-named line per custom field, so parseFRUOutput
+// counts occurrences to recover the real index for each one.
+var fruExtraBaseIndex = map[string]struct {
+ Area string
+ Base int
+}{
+ "Chassis Extra": {"c", 2},
+ "Board Extra": {"b", 5},
+ "Product Extra": {"p", 7},
}
func parseFRUOutput(output string) []fruField {
var fields []fruField
+ extraSeen := map[string]int{}
for _, line := range strings.Split(output, "\n") {
// Lines look like: " Field Name : value"
trimmed := strings.TrimLeft(line, " \t")
@@ -64,33 +79,32 @@ func parseFRUOutput(output string) []fruField {
continue
}
colon := strings.Index(trimmed, " : ")
+ valueOffset := 3
if colon < 0 {
// try ": " with no leading space before colon
colon = strings.Index(trimmed, ": ")
+ valueOffset = 2
if colon < 0 {
continue
}
- name := strings.TrimSpace(trimmed[:colon])
- value := strings.TrimSpace(trimmed[colon+2:])
- if name == "" {
- continue
- }
- editable, area, idx := fruFieldMeta(name)
- fields = append(fields, fruField{Name: name, Value: value, Editable: editable, Area: area, Index: idx})
- continue
}
name := strings.TrimSpace(trimmed[:colon])
- value := strings.TrimSpace(trimmed[colon+3:])
+ value := strings.TrimSpace(trimmed[colon+valueOffset:])
if name == "" {
continue
}
- editable, area, idx := fruFieldMeta(name)
+ editable, area, idx := fruFieldMeta(name, extraSeen)
fields = append(fields, fruField{Name: name, Value: value, Editable: editable, Area: area, Index: idx})
}
return fields
}
-func fruFieldMeta(name string) (editable bool, area string, index int) {
+func fruFieldMeta(name string, extraSeen map[string]int) (editable bool, area string, index int) {
+ if e, ok := fruExtraBaseIndex[name]; ok {
+ idx := e.Base + extraSeen[name]
+ extraSeen[name]++
+ return true, e.Area, idx
+ }
if e, ok := fruEditableFields[name]; ok {
return true, e.Area, e.Index
}
@@ -201,4 +215,3 @@ func runIPMIFRUWriteTask(ctx context.Context, j *jobState, exportDir string, p t
}
return nil
}
-
diff --git a/audit/internal/webui/ipmi_fru_test.go b/audit/internal/webui/ipmi_fru_test.go
new file mode 100644
index 0000000..4516818
--- /dev/null
+++ b/audit/internal/webui/ipmi_fru_test.go
@@ -0,0 +1,59 @@
+package webui
+
+import "testing"
+
+func TestParseFRUOutputExtraFields(t *testing.T) {
+ // Realistic ipmitool fru print output: repeated " Extra" lines
+ // (one per custom field) must resolve to sequential indices per the
+ // vendor FRU doc (Chassis Extra starts at 2, Board Extra at 5, Product
+ // Extra at 7), not all collapse onto the same index.
+ out := `
+ Product Manufacturer : Inspur
+ Product Name : NF5280M6
+ Product Part Number : PN123
+ Product Version : 1.0
+ Product Serial : SN123
+ Product Asset Tag : ASSET01
+ Product Extra : custom-p1
+ Board Mfg : Inspur
+ Board Product : BoardX
+ Board Serial : BSN1
+ Board Part Number : BPN1
+ Board Extra : custom-b1
+ Board Extra : custom-b2
+ Board Extra : custom-b3
+ Chassis Part Number : CPN1
+ Chassis Serial : CSN1
+ Chassis Extra : front-half
+ Chassis Extra : back-half
+`
+ fields := parseFRUOutput(out)
+
+ byName := map[string][]fruField{}
+ for _, f := range fields {
+ byName[f.Name] = append(byName[f.Name], f)
+ }
+
+ assertMeta := func(name string, occurrence int, wantArea string, wantIndex int) {
+ t.Helper()
+ list := byName[name]
+ if occurrence >= len(list) {
+ t.Fatalf("expected occurrence %d of %q, got %d entries", occurrence, name, len(list))
+ }
+ f := list[occurrence]
+ if f.Area != wantArea || f.Index != wantIndex {
+ t.Errorf("%s[%d] = area:%q index:%d, want area:%q index:%d", name, occurrence, f.Area, f.Index, wantArea, wantIndex)
+ }
+ if !f.Editable {
+ t.Errorf("%s[%d] expected editable", name, occurrence)
+ }
+ }
+
+ assertMeta("Product Asset Tag", 0, "p", 5)
+ assertMeta("Product Extra", 0, "p", 7)
+ assertMeta("Board Extra", 0, "b", 5)
+ assertMeta("Board Extra", 1, "b", 6)
+ assertMeta("Board Extra", 2, "b", 7)
+ assertMeta("Chassis Extra", 0, "c", 2)
+ assertMeta("Chassis Extra", 1, "c", 3)
+}