manta_server/server/handlers/
hw_cluster.rs

1//! Hardware cluster add/delete/configuration handlers.
2
3use axum::{Json, extract::Path, http::StatusCode, response::IntoResponse};
4
5use super::{ErrorResponse, RequestCtx, SiteHeader, to_handler_error};
6use crate::service;
7
8pub use manta_shared::types::api::hw_cluster::{
9  AddHwComponentRequest, ApplyHwConfigurationRequest, DeleteHwComponentRequest,
10  HwClusterMode,
11};
12
13/// `POST /api/v1/hardware-clusters/{target}/members` — move nodes matching a hardware pattern into a cluster.
14#[utoipa::path(post, path = "/hardware-clusters/{target}/members", tag = "hardware",
15  params(("target" = String, Path, description = "Target cluster name"), SiteHeader),
16  request_body = AddHwComponentRequest,
17  security(("bearerAuth" = [])),
18  responses(
19    // dry_run/real result union — kept as Value until the union shape is formalised
20    (status = 200, description = "Members added or preview", body = serde_json::Value),
21    (status = 401, description = "Unauthorized",             body = ErrorResponse),
22    (status = 500, description = "Internal error",           body = ErrorResponse),
23  )
24)]
25#[tracing::instrument(skip_all)]
26pub async fn add_hw_component(
27  ctx: RequestCtx,
28  Path(target): Path<String>,
29  Json(body): Json<AddHwComponentRequest>,
30) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
31  tracing::info!(
32    "add_hw_component target={} parent={} dry_run={}",
33    target,
34    body.parent_cluster,
35    body.dry_run
36  );
37  let infra = ctx.infra();
38
39  service::authorization::validate_user_group_access(
40    &infra, &ctx.token, &target,
41  )
42  .await
43  .map_err(to_handler_error)?;
44  service::authorization::validate_user_group_access(
45    &infra,
46    &ctx.token,
47    &body.parent_cluster,
48  )
49  .await
50  .map_err(to_handler_error)?;
51
52  let result = crate::service::hw_cluster::add_hw_component(
53    &infra,
54    &ctx.token,
55    &target,
56    &body.parent_cluster,
57    &body.pattern,
58    body.dry_run,
59    body.create_hsm_group,
60  )
61  .await
62  .map_err(to_handler_error)?;
63
64  Ok(Json(serde_json::json!({
65    "dry_run": body.dry_run,
66    "nodes_moved": result.nodes_moved,
67    "target_cluster": target,
68    "target_nodes": result.target_nodes,
69    "parent_cluster": body.parent_cluster,
70    "parent_nodes": result.parent_nodes,
71  })))
72}
73
74// ---------------------------------------------------------------------------
75// DELETE /api/v1/hardware-clusters/{target}/members
76// ---------------------------------------------------------------------------
77
78/// `DELETE /api/v1/hardware-clusters/{target}/members` — move nodes back to parent cluster by hardware pattern.
79#[utoipa::path(delete, path = "/hardware-clusters/{target}/members", tag = "hardware",
80  params(("target" = String, Path, description = "Target cluster name"), SiteHeader),
81  request_body = DeleteHwComponentRequest,
82  security(("bearerAuth" = [])),
83  responses(
84    // dry_run/real result union — kept as Value until the union shape is formalised
85    (status = 200, description = "Members removed or preview", body = serde_json::Value),
86    (status = 401, description = "Unauthorized",               body = ErrorResponse),
87    (status = 500, description = "Internal error",             body = ErrorResponse),
88  )
89)]
90#[tracing::instrument(skip_all)]
91pub async fn delete_hw_component(
92  ctx: RequestCtx,
93  Path(target): Path<String>,
94  Json(body): Json<DeleteHwComponentRequest>,
95) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
96  tracing::info!(
97    "delete_hw_component target={} parent={} dry_run={}",
98    target,
99    body.parent_cluster,
100    body.dry_run
101  );
102  let infra = ctx.infra();
103
104  service::authorization::validate_user_group_access(
105    &infra, &ctx.token, &target,
106  )
107  .await
108  .map_err(to_handler_error)?;
109  service::authorization::validate_user_group_access(
110    &infra,
111    &ctx.token,
112    &body.parent_cluster,
113  )
114  .await
115  .map_err(to_handler_error)?;
116
117  let result = crate::service::hw_cluster::delete_hw_component(
118    &infra,
119    &ctx.token,
120    &target,
121    &body.parent_cluster,
122    &body.pattern,
123    body.dry_run,
124    body.delete_hsm_group,
125  )
126  .await
127  .map_err(to_handler_error)?;
128
129  Ok(Json(serde_json::json!({
130    "dry_run": body.dry_run,
131    "nodes_moved": result.nodes_moved,
132    "target_cluster": target,
133    "target_nodes": result.target_nodes,
134    "parent_cluster": body.parent_cluster,
135    "parent_nodes": result.parent_nodes,
136  })))
137}
138
139// ---------------------------------------------------------------------------
140// POST /api/v1/hardware-clusters/{target}/configuration
141// ---------------------------------------------------------------------------
142
143/// `POST /api/v1/hardware-clusters/{target}/configuration` — pin or unpin nodes between clusters by hardware pattern.
144#[utoipa::path(post, path = "/hardware-clusters/{target}/configuration", tag = "hardware",
145  params(("target" = String, Path, description = "Target cluster name"), SiteHeader),
146  request_body = ApplyHwConfigurationRequest,
147  security(("bearerAuth" = [])),
148  responses(
149    // dry_run/real result union — kept as Value until the union shape is formalised
150    (status = 200, description = "Configuration applied or preview", body = serde_json::Value),
151    (status = 401, description = "Unauthorized",                     body = ErrorResponse),
152    (status = 500, description = "Internal error",                   body = ErrorResponse),
153  )
154)]
155#[tracing::instrument(skip_all)]
156pub async fn apply_hw_configuration(
157  ctx: RequestCtx,
158  Path(target): Path<String>,
159  Json(body): Json<ApplyHwConfigurationRequest>,
160) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
161  tracing::info!(
162    "apply_hw_configuration target={} parent={} dry_run={}",
163    target,
164    body.parent_cluster,
165    body.dry_run
166  );
167  let infra = ctx.infra();
168
169  service::authorization::validate_user_group_access(
170    &infra, &ctx.token, &target,
171  )
172  .await
173  .map_err(to_handler_error)?;
174  service::authorization::validate_user_group_access(
175    &infra,
176    &ctx.token,
177    &body.parent_cluster,
178  )
179  .await
180  .map_err(to_handler_error)?;
181
182  let result = crate::service::hw_cluster::apply_hw_configuration(
183    &infra,
184    &ctx.token,
185    crate::service::hw_cluster::ApplyHwConfigurationParams {
186      mode: body.mode,
187      target_group_name: &target,
188      parent_group_name: &body.parent_cluster,
189      pattern: &body.pattern,
190      dryrun: body.dry_run,
191      create_target_group: body.create_target_hsm_group,
192      delete_empty_parent_group: body.delete_empty_parent_hsm_group,
193    },
194  )
195  .await
196  .map_err(to_handler_error)?;
197
198  Ok(Json(serde_json::json!({
199    "dry_run": body.dry_run,
200    "target_cluster": target,
201    "target_nodes": result.target_nodes,
202    "parent_cluster": body.parent_cluster,
203    "parent_nodes": result.parent_nodes,
204  })))
205}