Add shutdown-prep button to dashboard-v2 NodeDetail
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Failing after 3s
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Failing after 3s
Wires safe-shutdown-prep.sh into the dashboard so an operator can prep a node for hardware maintenance from a browser instead of SSH. New RPC methods (0x20/0x21): - ShutdownPrepCheck runs `--dry-run` to completion and returns the full report. Never stops anything, safe to call repeatedly. - ShutdownPrepExecute starts the real run detached (`systemd-run --user --scope --collect`), placing it in a cgroup outside claw-store.service's own -- the script's own step 6 stops that service, i.e. the process that would otherwise be running it, so it has to survive its own parent dying. Returns immediately with a "started" message; full output lands in /var/lib/claw-store/shutdown-prep.log for whoever's at the machine once it's gone dark, since there's no way to stream a live result past the point the daemon stops itself. - Execute double-checks confirm_node_name against the peer's own configured name server-side, on top of the aggregator's own path match -- defense in depth for a highly consequential action. Aggregator endpoints (admin-token gated, AuthedCaller::require_admin): POST /api/v2/node/:name/shutdown-prep/check POST /api/v2/node/:name/shutdown-prep/execute Frontend: ShutdownPrepPanel on NodeDetail. Check button always enabled; the real "stop services" button only unlocks after a ready check, and additionally requires typing the exact node name to confirm before it's clickable. Also fixes a script bug found while testing this against the live daemon process (not caught in manual interactive-shell testing): the zpool-detection line parsed raw `mount` output positionally, which returned the wrong field under the daemon's process context for reasons that didn't reproduce interactively. Switched to `df --output=source`, which is stable across both. Verified end-to-end against tank, architect, and morpheus, including cross-node targeting (tank's dashboard successfully triggered a check on morpheus over the fleet RPC layer). Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
||||
import { useState } from 'react';
|
||||
import { api } from '../lib/api';
|
||||
/**
|
||||
* Node-maintenance panel: runs `safe-shutdown-prep.sh --dry-run` on
|
||||
* demand (safe, read-mostly, never stops anything) and — only once
|
||||
* that comes back ready — unlocks a type-to-confirm button that
|
||||
* starts the real run.
|
||||
*
|
||||
* The real run is fire-and-forget by necessity: its own steps stop
|
||||
* this node's daemon, which is what's serving this very page, so
|
||||
* there is no way to stream a live result past that point. Once
|
||||
* started, the UI says so plainly and points at the on-disk log for
|
||||
* the full report.
|
||||
*/
|
||||
export function ShutdownPrepPanel({ name }) {
|
||||
const [check, setCheck] = useState({ phase: 'idle' });
|
||||
const [exec, setExec] = useState({ phase: 'idle' });
|
||||
const [confirmText, setConfirmText] = useState('');
|
||||
const runCheck = () => {
|
||||
setCheck({ phase: 'checking' });
|
||||
setExec({ phase: 'idle' });
|
||||
setConfirmText('');
|
||||
api
|
||||
.shutdownPrepCheck(name)
|
||||
.then((r) => setCheck({ phase: 'done', ready: r.ready, output: r.output }))
|
||||
.catch((e) => setCheck({ phase: 'error', message: String(e) }));
|
||||
};
|
||||
const runExecute = () => {
|
||||
if (confirmText !== name)
|
||||
return;
|
||||
setExec({ phase: 'starting' });
|
||||
api
|
||||
.shutdownPrepExecute(name, confirmText)
|
||||
.then((r) => setExec({ phase: 'started', message: r.message }))
|
||||
.catch((e) => setExec({ phase: 'error', message: String(e) }));
|
||||
};
|
||||
const ready = check.phase === 'done' && check.ready;
|
||||
return (_jsxs("div", { className: "rounded border border-slate-800 bg-slate-900/40 p-4 text-sm space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { className: "text-slate-500 uppercase text-xs tracking-wider", children: "shutdown prep" }), _jsx("button", { onClick: runCheck, disabled: check.phase === 'checking', className: "px-3 py-1.5 rounded bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs disabled:opacity-50", children: check.phase === 'checking' ? 'checking…' : 'check readiness for shutdown' })] }), _jsxs("p", { className: "text-xs text-slate-500", children: ["Runs a dry-run of the pre-shutdown checklist on ", _jsx("span", { className: "font-mono", children: name }), ' ', "\u2014 active builds, pending peer sync, a final snapshot + replicate to cold. Nothing is stopped or unmounted by this check."] }), check.phase === 'error' && (_jsx("div", { className: "rounded border border-red-800 bg-red-950/40 p-3 text-red-300 text-xs", children: check.message })), check.phase === 'done' && (_jsxs(_Fragment, { children: [_jsx("div", { className: `rounded border p-3 text-xs ${check.ready
|
||||
? 'border-emerald-800 bg-emerald-950/40 text-emerald-300'
|
||||
: 'border-amber-800 bg-amber-950/40 text-amber-300'}`, children: check.ready
|
||||
? '✓ ready — safe to start the real shutdown-prep run'
|
||||
: '! not ready — see output below (active build or un-synced changes are the usual cause)' }), _jsx("pre", { className: "max-h-72 overflow-auto rounded bg-slate-950 border border-slate-800 p-3 text-[11px] leading-relaxed text-slate-300 whitespace-pre-wrap", children: check.output })] })), ready && exec.phase !== 'started' && (_jsxs("div", { className: "rounded border border-red-900 bg-red-950/30 p-3 space-y-2", children: [_jsxs("div", { className: "text-red-300 text-xs", children: ["This starts the real run: stops maintenance timers, the dashboard, the storage daemon (gossip announces departure to peers), and unmounts FUSE on", ' ', _jsx("span", { className: "font-mono", children: name }), ". The node's dashboard connection will drop partway through \u2014 that's expected, not an error. It does ", _jsx("strong", { children: "not" }), " power the machine off; do that yourself once it's gone dark."] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { value: confirmText, onChange: (e) => setConfirmText(e.target.value), placeholder: `type "${name}" to confirm`, className: "flex-1 rounded bg-slate-900 border border-slate-700 px-2 py-1.5 text-xs font-mono text-slate-200 placeholder:text-slate-600" }), _jsx("button", { onClick: runExecute, disabled: confirmText !== name || exec.phase === 'starting', className: "px-3 py-1.5 rounded bg-red-900 hover:bg-red-800 text-red-100 text-xs disabled:opacity-40 disabled:cursor-not-allowed whitespace-nowrap", children: exec.phase === 'starting' ? 'starting…' : `stop services on ${name}` })] })] })), exec.phase === 'error' && (_jsx("div", { className: "rounded border border-red-800 bg-red-950/40 p-3 text-red-300 text-xs", children: exec.message })), exec.phase === 'started' && (_jsx("div", { className: "rounded border border-sky-800 bg-sky-950/30 p-3 text-sky-300 text-xs", children: exec.message }))] }));
|
||||
}
|
||||
Reference in New Issue
Block a user