manta_server/server/handlers/
power.rs

1//! POST /api/v1/power.
2
3use axum::{Json, http::StatusCode, response::IntoResponse};
4use manta_backend_dispatcher::interfaces::hsm::group::GroupTrait;
5use serde::Deserialize;
6use utoipa::ToSchema;
7
8use super::{ErrorResponse, RequestCtx, SiteHeader, to_handler_error};
9use crate::service;
10
11// ---------------------------------------------------------------------------
12// POST /api/v1/power — Power on/off/reset nodes or cluster
13// ---------------------------------------------------------------------------
14
15/// Power action to apply to the target nodes or cluster.
16#[derive(Debug, Deserialize, ToSchema)]
17#[serde(rename_all = "lowercase")]
18pub enum PowerAction {
19  /// Power on the nodes.
20  On,
21  /// Power off the nodes.
22  Off,
23  /// Power-cycle (reset) the nodes.
24  Reset,
25}
26
27/// Whether `targets` contains xnames (`nodes`) or a single cluster name (`cluster`).
28#[derive(Debug, Deserialize, ToSchema)]
29#[serde(rename_all = "lowercase")]
30pub enum PowerTargetType {
31  /// `targets` is a list of xnames.
32  Nodes,
33  /// `targets` contains a single HSM group name whose members will be targeted.
34  Cluster,
35}
36
37/// Request body for `POST /power`.
38#[derive(Deserialize, ToSchema)]
39pub struct PowerRequest {
40  /// Power operation to perform.
41  pub action: PowerAction,
42  /// For nodes: hosts expression (xnames, nids, or hostlist notation).
43  /// For cluster: the HSM group name.
44  pub targets_expression: String,
45  /// Indicates whether `targets_expression` is a node expression or a cluster name.
46  pub target_type: PowerTargetType,
47  /// Pass `--force` to the underlying power operation (forceful shutdown/reset).
48  #[serde(default)]
49  pub force: bool,
50}
51
52/// `POST /api/v1/power` — power on, off, or reset nodes or all members of a cluster.
53#[utoipa::path(post, path = "/power", tag = "power",
54  params(SiteHeader),
55  request_body = PowerRequest,
56  security(("bearerAuth" = [])),
57  responses(
58    (status = 200, description = "Power operation result", body = serde_json::Value),
59    (status = 400, description = "Bad request",            body = ErrorResponse),
60    (status = 401, description = "Unauthorized",           body = ErrorResponse),
61    (status = 500, description = "Internal error",         body = ErrorResponse),
62  )
63)]
64#[tracing::instrument(skip_all)]
65pub async fn post_power(
66  ctx: RequestCtx,
67  Json(body): Json<PowerRequest>,
68) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
69  tracing::info!(
70    "post_power action={:?} target_type={:?}",
71    body.action,
72    body.target_type
73  );
74  let infra = ctx.infra();
75
76  let xnames: Vec<String> = match body.target_type {
77    PowerTargetType::Cluster => infra
78      .backend
79      .get_member_vec_from_group_name_vec(
80        &ctx.token,
81        std::slice::from_ref(&body.targets_expression),
82      )
83      .await
84      .map_err(to_handler_error)?,
85    PowerTargetType::Nodes => {
86      crate::server::common::node_ops::resolve_hosts_expression(
87        infra.backend,
88        &ctx.token,
89        &body.targets_expression,
90        false,
91      )
92      .await
93      .map_err(to_handler_error)?
94    }
95  };
96
97  if xnames.is_empty() {
98    return Err((
99      StatusCode::BAD_REQUEST,
100      Json(ErrorResponse {
101        error: "No nodes to operate on".into(),
102      }),
103    ));
104  }
105
106  let params = service::power::ApplyPowerParams {
107    action: match body.action {
108      PowerAction::On => service::power::PowerAction::On,
109      PowerAction::Off => service::power::PowerAction::Off,
110      PowerAction::Reset => service::power::PowerAction::Reset,
111    },
112    xnames,
113    force: body.force,
114  };
115  let result = service::power::apply_power(&infra, &ctx.token, &params)
116    .await
117    .map_err(to_handler_error)?;
118
119  Ok(Json(result))
120}