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:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user