manta_server/server/handlers/
power.rs

1//! Power endpoints.
2//!
3//! - `POST /api/v1/power` starts a PCS power transition and returns
4//!   immediately with `{ transition_id, operation }`. The CLI then
5//!   polls the next endpoint until the transition reports `completed`.
6//! - `GET /api/v1/power/transitions/{id}` returns the current snapshot
7//!   of the named transition (status + task counts + per-task detail).
8//!
9//! The two-step shape (start → poll) is dictated by PCS itself: a
10//! transition is a long-running cluster-wide operation, and the
11//! caller is expected to drive completion off the snapshot rather
12//! than blocking the request. Both handlers delegate to
13//! [`crate::service::power`] — target-xname resolution is shared with
14//! the `boot-parameters` and `kernel-parameters` flows.
15
16use axum::{Json, extract::Path, http::StatusCode, response::IntoResponse};
17
18use super::{ErrorResponse, RequestCtx, SiteHeader, to_handler_error};
19use crate::service;
20
21// ---------------------------------------------------------------------------
22// POST /api/v1/power — Power on/off/reset nodes or cluster
23// ---------------------------------------------------------------------------
24
25pub use manta_shared::types::api::power::{
26  PowerAction, PowerRequest, PowerTargetType,
27};
28
29/// `POST /api/v1/power` — start a PCS power transition (on / off /
30/// reset) against nodes or all members of a cluster and return the
31/// transition id **immediately**. Does not block until the
32/// transition completes — the CLI is responsible for polling
33/// `GET /power/transitions/{id}` until the snapshot reports
34/// `transitionStatus = "completed"`.
35///
36/// Returns a `TransitionStartOutput` (`{ transitionID, operation }`)
37/// as JSON. Callers can hand the `transitionID` to
38/// [`get_power_transition`].
39#[utoipa::path(post, path = "/power", tag = "power",
40  params(SiteHeader),
41  request_body = PowerRequest,
42  security(("bearerAuth" = [])),
43  responses(
44    // TransitionStartOutput lives in manta-backend-dispatcher (third-party,
45    // no ToSchema) — kept as Value until upstream derives it.
46    (status = 200, description = "PCS transition started; returns TransitionStartOutput", body = serde_json::Value),
47    (status = 400, description = "Bad request",            body = ErrorResponse),
48    (status = 401, description = "Unauthorized",           body = ErrorResponse),
49    (status = 500, description = "Internal error",         body = ErrorResponse),
50  )
51)]
52#[tracing::instrument(skip_all)]
53pub async fn post_power(
54  ctx: RequestCtx,
55  Json(body): Json<PowerRequest>,
56) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
57  tracing::info!(
58    "post_power action={:?} target_type={:?}",
59    body.action,
60    body.target_type
61  );
62  let infra = ctx.infra();
63
64  let xnames = service::power::resolve_target_xnames(
65    &infra,
66    &ctx.token,
67    body.target_type,
68    &body.host_expression,
69  )
70  .await
71  .map_err(to_handler_error)?;
72
73  let params = service::power::ApplyPowerParams {
74    action: body.action,
75    xnames,
76    force: body.force,
77  };
78  let result = service::power::apply_power(&infra, &ctx.token, &params)
79    .await
80    .map_err(to_handler_error)?;
81
82  Ok(Json(result))
83}
84
85// ---------------------------------------------------------------------------
86// GET /api/v1/power/transitions/{id} — Snapshot a PCS transition
87// ---------------------------------------------------------------------------
88
89/// `GET /api/v1/power/transitions/{id}` — fetch the current snapshot
90/// of a PCS power transition (status, task counts, per-task detail).
91/// Called by the CLI's poll loop after `POST /power` returns the id.
92#[utoipa::path(get, path = "/power/transitions/{id}", tag = "power",
93  params(SiteHeader),
94  security(("bearerAuth" = [])),
95  responses(
96    // TransitionResponse lives in manta-backend-dispatcher (third-party,
97    // no ToSchema) — kept as Value until upstream derives it.
98    (status = 200, description = "Transition snapshot",  body = serde_json::Value),
99    (status = 401, description = "Unauthorized",         body = ErrorResponse),
100    (status = 404, description = "Unknown transition id",body = ErrorResponse),
101    (status = 500, description = "Internal error",       body = ErrorResponse),
102  )
103)]
104#[tracing::instrument(skip_all)]
105pub async fn get_power_transition(
106  ctx: RequestCtx,
107  Path(id): Path<String>,
108) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
109  tracing::debug!("get_power_transition id={id}");
110  let infra = ctx.infra();
111  let snapshot = service::power::get_power_transition(&infra, &ctx.token, &id)
112    .await
113    .map_err(to_handler_error)?;
114  Ok(Json(snapshot))
115}