Skip to main content

manta_server/server/handlers/
hw_cluster.rs

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