From 7aa276320b410dcb7bedf4220357897bfa70fb47 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Mon, 24 Aug 2026 18:39:02 +0300 Subject: [PATCH] fix(webui): trust IPMI FRU readback over ipmitool's exit code `ipmitool fru edit` frequently exits non-zero after a field resize (shrinking/growing a string shifts every later offset in the FRU record) even though the write itself landed correctly on the BMC - the failure is in ipmitool's own post-write re-parse, not the write. bee was treating that exit code as authoritative and marking the task failed even when the field was actually written. runIPMIFRUWriteTask now re-reads the FRU on a non-zero exit and compares the target field's actual value before deciding the task failed, logging which path it took either way. Co-Authored-By: Claude Sonnet 5 --- audit/internal/webui/ipmi_fru.go | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/audit/internal/webui/ipmi_fru.go b/audit/internal/webui/ipmi_fru.go index f99c9f6..fcfdf6c 100644 --- a/audit/internal/webui/ipmi_fru.go +++ b/audit/internal/webui/ipmi_fru.go @@ -210,8 +210,37 @@ func runIPMIFRUWriteTask(ctx context.Context, j *jobState, exportDir string, p t j.append(fmt.Sprintf("Setting %s (%s %d) = %q", c.Name, c.Area, c.Index, c.Value)) cmd := exec.CommandContext(ctx, "ipmitool", "fru", "edit", "0", "field", c.Area, fmt.Sprintf("%d", c.Index), c.Value) if err := streamCmdJob(j, cmd); err != nil { - return fmt.Errorf("fru edit %s %d: %w", c.Area, c.Index, err) + // ipmitool's `fru edit` frequently exits non-zero after a field + // resize (shrinking/growing a string shifts every later offset in + // the FRU record) even though the write itself landed correctly - + // the failure is in ipmitool's own post-write re-parse, not the + // BMC write. Read the FRU back and trust the BMC's own data over + // ipmitool's exit code before declaring the task failed. + ok, verifyErr := verifyFRUField(ctx, c.Area, c.Index, c.Value) + if verifyErr != nil { + j.append("Readback verification failed: " + verifyErr.Error()) + return fmt.Errorf("fru edit %s %d: %w", c.Area, c.Index, err) + } + if !ok { + return fmt.Errorf("fru edit %s %d: %w", c.Area, c.Index, err) + } + j.append(fmt.Sprintf("ipmitool exited with an error, but readback confirms %s %d was written correctly - continuing", c.Area, c.Index)) } } return nil } + +// verifyFRUField re-reads the FRU and checks whether the field at area/index +// already holds want, independent of any exit code from a prior edit. +func verifyFRUField(ctx context.Context, area string, index int, want string) (bool, error) { + out, err := exec.CommandContext(ctx, "ipmitool", "fru", "print", "0").CombinedOutput() + if err != nil { + return false, fmt.Errorf("fru print: %w", err) + } + for _, f := range parseFRUOutput(string(out)) { + if f.Area == area && f.Index == index { + return f.Value == want, nil + } + } + return false, nil +}