Skip to main content

manta_server/server/handlers/
power.rs

1//! Power endpoints.
2//!
3//! - `POST /v2/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 /v2/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::server::audit::AuditNote;
20use crate::service;
21
22// ---------------------------------------------------------------------------
23// POST /v2/power — Power on/off/reset nodes or cluster
24// ---------------------------------------------------------------------------
25
26pub use manta_shared::types::api::power::{
27  PowerAction, PowerRequest, PowerTargetType,
28};
29
30/// `POST /v2/power` — start a PCS power transition (on / off /
31/// reset) against nodes or all members of a cluster and return the
32/// transition id **immediately**. Does not block until the
33/// transition completes — the CLI is responsible for polling
34/// `GET /power/transitions/{id}` until the snapshot reports
35/// `transitionStatus = "completed"`.
36///
37/// Returns a `TransitionStartOutput` (`{ transitionID, operation }`)
38/// as JSON. Callers can hand the `transitionID` to
39/// [`get_power_transition`].
40#[utoipa::path(post, path = "/power", tag = "power",
41  params(SiteHeader),
42  request_body = PowerRequest,
43  security(("bearerAuth" = [])),
44  responses(
45    // TransitionStartOutput lives in manta-backend-dispatcher (third-party,
46    // no ToSchema) — kept as Value until upstream derives it.
47    (status = 200, description = "PCS transition started; returns TransitionStartOutput", body = serde_json::Value),
48    (status = 400, description = "Bad request",            body = ErrorResponse),
49    (status = 401, description = "Unauthorized",           body = ErrorResponse),
50    (status = 500, description = "Internal error",         body = ErrorResponse),
51  )
52)]
53#[tracing::instrument(skip_all)]
54pub async fn post_power(
55  ctx: RequestCtx,
56  note: AuditNote,
57  Json(body): Json<PowerRequest>,
58) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
59  note
60    .action(match body.action {
61      PowerAction::On => "power.on",
62      PowerAction::Off => "power.off",
63      PowerAction::Reset => "power.reset",
64    })
65    .target(
66      match body.target_type {
67        PowerTargetType::Nodes => "hosts",
68        PowerTargetType::Cluster => "group",
69      },
70      &body.host_expression,
71    )
72    .detail("force", body.force);
73
74  tracing::info!(
75    "post_power action={:?} target_type={:?}",
76    body.action,
77    body.target_type
78  );
79  let infra = ctx.infra();
80
81  let xnames = service::power::resolve_target_xnames(
82    &infra,
83    &ctx.token,
84    body.target_type,
85    &body.host_expression,
86  )
87  .await
88  .map_err(to_handler_error)?;
89  note.nodes(&xnames);
90
91  let params = service::power::ApplyPowerParams {
92    action: body.action,
93    xnames,
94    force: body.force,
95  };
96  let result = service::power::apply_power(&infra, &ctx.token, &params)
97    .await
98    .map_err(to_handler_error)?;
99  note.produced("transition", &result.transition_id);
100
101  Ok(Json(result))
102}
103
104// ---------------------------------------------------------------------------
105// GET /v2/power/transitions/{id} — Snapshot a PCS transition
106// ---------------------------------------------------------------------------
107
108/// `GET /v2/power/transitions/{id}` — fetch the current snapshot
109/// of a PCS power transition (status, task counts, per-task detail).
110/// Called by the CLI's poll loop after `POST /power` returns the id.
111#[utoipa::path(get, path = "/power/transitions/{id}", tag = "power",
112  params(SiteHeader),
113  security(("bearerAuth" = [])),
114  responses(
115    // TransitionResponse lives in manta-backend-dispatcher (third-party,
116    // no ToSchema) — kept as Value until upstream derives it.
117    (status = 200, description = "Transition snapshot",  body = serde_json::Value),
118    (status = 401, description = "Unauthorized",         body = ErrorResponse),
119    (status = 404, description = "Unknown transition id",body = ErrorResponse),
120    (status = 500, description = "Internal error",       body = ErrorResponse),
121  )
122)]
123#[tracing::instrument(skip_all)]
124pub async fn get_power_transition(
125  ctx: RequestCtx,
126  Path(id): Path<String>,
127) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
128  tracing::debug!("get_power_transition id={id}");
129  let infra = ctx.infra();
130  let snapshot = service::power::get_power_transition(&infra, &ctx.token, &id)
131    .await
132    .map_err(to_handler_error)?;
133  Ok(Json(snapshot))
134}