Add shutdown-prep button to dashboard-v2 NodeDetail
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:
osobh
2026-07-31 14:46:57 -07:00
co-authored by Claude Sonnet 5
parent 6a4bc09cbb
commit 4ea1cbed2e
13 changed files with 597 additions and 4 deletions
+39
View File
@@ -229,6 +229,45 @@ pub async fn call_repo_release(
serde_json::from_slice(&reply).context("decoding RepoReleaseReply JSON")
}
/// Convenience wrapper for [`Method::ShutdownPrepCheck`]. Runs
/// `safe-shutdown-prep.sh --dry-run` on the connected peer and waits
/// for the full report. Never stops anything on the peer.
pub async fn call_shutdown_prep_check(
conn: &Connection,
) -> Result<crate::cluster::shutdown_prep::ShutdownPrepCheckReply> {
let req = crate::cluster::shutdown_prep::ShutdownPrepCheckRequest {};
let payload = serde_json::to_vec(&req).context("encoding ShutdownPrepCheckRequest")?;
let reply = rpc_call(conn, Method::ShutdownPrepCheck, &payload).await?;
if reply.len() == 1 {
if let Some(code) = decode_error(reply[0]) {
bail!("peer replied with error: {}", code.describe());
}
}
serde_json::from_slice(&reply).context("decoding ShutdownPrepCheckReply JSON")
}
/// Convenience wrapper for [`Method::ShutdownPrepExecute`]. Starts the
/// real shutdown-prep run on the connected peer (detached — this call
/// returns as soon as the peer confirms it started, not when it
/// finishes, since the peer's own daemon stops itself partway
/// through).
pub async fn call_shutdown_prep_execute(
conn: &Connection,
confirm_node_name: &str,
) -> Result<crate::cluster::shutdown_prep::ShutdownPrepExecuteReply> {
let req = crate::cluster::shutdown_prep::ShutdownPrepExecuteRequest {
confirm_node_name: confirm_node_name.to_string(),
};
let payload = serde_json::to_vec(&req).context("encoding ShutdownPrepExecuteRequest")?;
let reply = rpc_call(conn, Method::ShutdownPrepExecute, &payload).await?;
if reply.len() == 1 {
if let Some(code) = decode_error(reply[0]) {
bail!("peer replied with error: {}", code.describe());
}
}
serde_json::from_slice(&reply).context("decoding ShutdownPrepExecuteReply JSON")
}
/// Recognise a single-byte reply as one of our error codes. Returns
/// `None` for any other single-byte value (which is a valid reply,
/// just an unusually short one).