From 796acdfec15a48fac6729c4c33ae66b117a218f1 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Wed, 1 Jul 2026 17:21:26 +0300 Subject: [PATCH] ipmi fru: add Asset Tag and vendor Extra field write support (in-band) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Product Asset Tag (p 5) and the repeated custom "Extra" fields (Product Extra p 7, Board Extra b 5/6/7, Chassis Extra c 2/3) from the Inspur FRU field doc weren't writable — ipmitool prints identically-named lines for each custom field with no index of its own, so a plain name lookup couldn't tell them apart. parseFRUOutput now counts occurrences per area to recover the real index, and the existing area/index round-trip in the FRU editor write path picks it up automatically. Out-of-band (-H/-U/-P) writing remains out of scope. Co-Authored-By: Claude Sonnet 5 --- audit/internal/webui/ipmi_fru.go | 61 ++++++++++++++++----------- audit/internal/webui/ipmi_fru_test.go | 59 ++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 24 deletions(-) create mode 100644 audit/internal/webui/ipmi_fru_test.go 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) +}