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 +}