From d0ea29d2a9178106e6e36d2b277e27c77054f49e Mon Sep 17 00:00:00 2001 From: Ilker Manap Date: Thu, 29 Oct 2026 22:37:02 +0100 Subject: [PATCH] Power: say why a BMC command failed (CLI, UI); certificate changes are events Found in the lab test of certificate pinning: the CLI printed only 'ipmitool failed on 1 machine(s)'. Co-Authored-By: Claude Opus 5.5 --- cmd/rollcall/client.go | 7 +++++-- cmd/rollcall/ipmi.go | 11 ++++++++++- cmd/rollcall/server.go | 6 +++++- cmd/rollcall/web/app.js | 2 +- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/cmd/rollcall/client.go b/cmd/rollcall/client.go index 3a89cbb..cb51ba0 100644 --- a/cmd/rollcall/client.go +++ b/cmd/rollcall/client.go @@ -232,13 +232,16 @@ func runClient(cmd string, args []string) error { if len(ids) > 1 { fmt.Printf("%s: ", id) } - fmt.Println(str(d["output"])) + if o := str(d["output"]); o != "" { + fmt.Println(o) + } if ok, _ := d["ok"].(bool); !ok { + fmt.Println("error:", orDefault(str(d["error"]), "the BMC command failed")) failed++ } } if failed > 0 { - return fmt.Errorf("ipmitool failed on %d machine(s)", failed) + return fmt.Errorf("power %s failed on %d machine(s)", args[1], failed) } case "bootdev": _ = need(2) diff --git a/cmd/rollcall/ipmi.go b/cmd/rollcall/ipmi.go index 2a25010..1a64624 100644 --- a/cmd/rollcall/ipmi.go +++ b/cmd/rollcall/ipmi.go @@ -72,7 +72,16 @@ func (a *App) power(ctx context.Context, m *Machine, op, arg string) (string, er if c, err = a.pinBMC(ctx, m, c); err != nil { return "", err } - return a.powerVia(ctx, a.rackOf(m), c, op, arg) + out, err := a.powerVia(ctx, a.rackOf(m), c, op, arg) + a.noteCertChange(ctx, m, err) + return out, err +} + +// noteCertChange puts a refused BMC certificate on the machine's events. +func (a *App) noteCertChange(ctx context.Context, m *Machine, err error) { + if err != nil && strings.Contains(err.Error(), "certificate changed") { + _ = a.store.AddEvent(ctx, m.ID, "bmc certificate changed", oneLine(err.Error())) + } } // pinBMC records the certificate of an HTTPS BMC (Redfish, Proxmox) before diff --git a/cmd/rollcall/server.go b/cmd/rollcall/server.go index 158592d..1111031 100644 --- a/cmd/rollcall/server.go +++ b/cmd/rollcall/server.go @@ -786,7 +786,11 @@ func (a *App) handlePower(w http.ResponseWriter, r *http.Request) { if action != "status" { _ = a.store.AddEvent(r.Context(), m.ID, "power "+action, out) } - writeJSON(w, 200, obj{"id": m.ID, "action": action, "ok": err == nil, "output": out}) + res := obj{"id": m.ID, "action": action, "ok": err == nil, "output": out} + if err != nil { + res["error"] = err.Error() + } + writeJSON(w, 200, res) } func (a *App) handleBootdev(w http.ResponseWriter, r *http.Request) { diff --git a/cmd/rollcall/web/app.js b/cmd/rollcall/web/app.js index 857fc59..66720d9 100644 --- a/cmd/rollcall/web/app.js +++ b/cmd/rollcall/web/app.js @@ -355,7 +355,7 @@ async function each(ids, label, fn) { await refresh(true); } const act = { - power: (ids, action) => each(ids, `power ${action}`, async id => { const r = await api('POST', `/api/v1/machines/${id}/power`, { action }); return r.output || (r.ok ? 'ok' : 'failed'); }), + power: (ids, action) => each(ids, `power ${action}`, async id => { const r = await api('POST', `/api/v1/machines/${id}/power`, { action }); if (!r.ok) throw new Error(r.error || r.output || 'the BMC command failed'); return r.output || 'ok'; }), bootdev: (ids, device) => each(ids, `bootdev ${device}`, async id => { const r = await api('POST', `/api/v1/machines/${id}/bootdev`, { device }); return r.output || (r.ok ? 'ok' : 'failed'); }), async deploy(ids) { const one = ids.length === 1 ? S.machines.find(m => m.id === ids[0]) : null;