manta_server/server/handlers/
boot_parameters.rs

1//! Boot-parameters + apply_boot_config handlers.
2
3use axum::{Json, extract::Query, http::StatusCode, response::IntoResponse};
4use serde::Deserialize;
5use utoipa::{IntoParams, ToSchema};
6
7use super::{
8  ErrorResponse, RequestCtx, SiteHeader, serialize_or_500, to_handler_error,
9};
10use crate::service;
11
12// ---------------------------------------------------------------------------
13// GET /api/v1/boot-parameters
14// ---------------------------------------------------------------------------
15
16/// Query parameters for `GET /boot-parameters`.
17#[derive(Deserialize, IntoParams)]
18pub struct BootParametersQuery {
19  /// HSM group whose members' boot parameters should be returned.
20  pub hsm_group: Option<String>,
21  /// Explicit comma-separated xnames; mutually exclusive with
22  /// `hsm_group`.
23  pub nodes: Option<String>,
24}
25
26/// GET /boot-parameters — fetch BSS boot parameters for a group or node list.
27#[utoipa::path(get, path = "/boot-parameters", tag = "boot-parameters",
28  params(BootParametersQuery, SiteHeader),
29  security(("bearerAuth" = [])),
30  responses(
31    (status = 200, description = "Boot parameters",  body = serde_json::Value),
32    (status = 401, description = "Unauthorized",     body = ErrorResponse),
33    (status = 500, description = "Internal error",   body = ErrorResponse),
34  )
35)]
36#[tracing::instrument(skip_all)]
37pub async fn get_boot_parameters(
38  ctx: RequestCtx,
39  Query(q): Query<BootParametersQuery>,
40) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
41  let infra = ctx.infra();
42
43  let params = service::boot_parameters::GetBootParametersParams {
44    hsm_group: q.hsm_group,
45    nodes: q.nodes,
46    settings_hsm_group_name: None,
47  };
48
49  let boot_params =
50    service::boot_parameters::get_boot_parameters(&infra, &ctx.token, &params)
51      .await
52      .map_err(to_handler_error)?;
53
54  Ok(Json(boot_params))
55}
56
57// ---------------------------------------------------------------------------
58// DELETE /api/v1/boot-parameters
59// ---------------------------------------------------------------------------
60
61/// Body for `DELETE /boot-parameters`.
62#[derive(Deserialize, ToSchema)]
63pub struct DeleteBootParametersRequest {
64  /// Xnames whose BSS boot-parameter entries should be deleted.
65  pub hosts: Vec<String>,
66}
67
68/// DELETE /boot-parameters — remove BSS boot parameter entries for specified hosts.
69#[utoipa::path(delete, path = "/boot-parameters", tag = "boot-parameters",
70  params(SiteHeader),
71  request_body = DeleteBootParametersRequest,
72  security(("bearerAuth" = [])),
73  responses(
74    (status = 204, description = "Boot parameters removed"),
75    (status = 400, description = "Bad request",      body = ErrorResponse),
76    (status = 401, description = "Unauthorized",     body = ErrorResponse),
77    (status = 500, description = "Internal error",   body = ErrorResponse),
78  )
79)]
80#[tracing::instrument(skip_all)]
81pub async fn delete_boot_parameters(
82  ctx: RequestCtx,
83  Json(body): Json<DeleteBootParametersRequest>,
84) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
85  if body.hosts.is_empty() {
86    return Err((
87      StatusCode::BAD_REQUEST,
88      Json(ErrorResponse {
89        error: "hosts list must not be empty".to_string(),
90      }),
91    ));
92  }
93  tracing::info!("delete_boot_parameters hosts={:?}", body.hosts);
94  let infra = ctx.infra();
95
96  service::boot_parameters::delete_boot_parameters(
97    &infra, &ctx.token, body.hosts,
98  )
99  .await
100  .map_err(to_handler_error)?;
101
102  Ok(StatusCode::NO_CONTENT)
103}
104
105// ---------------------------------------------------------------------------
106// POST /api/v1/boot-parameters
107// ---------------------------------------------------------------------------
108
109/// POST /boot-parameters — create a new BSS boot parameters entry.
110#[utoipa::path(post, path = "/boot-parameters", tag = "boot-parameters",
111  params(SiteHeader),
112  request_body = manta_backend_dispatcher::types::bss::BootParameters,
113  security(("bearerAuth" = [])),
114  responses(
115    (status = 201, description = "Boot parameters created",  body = serde_json::Value),
116    (status = 401, description = "Unauthorized",             body = ErrorResponse),
117    (status = 500, description = "Internal error",           body = ErrorResponse),
118  )
119)]
120#[tracing::instrument(skip_all)]
121pub async fn add_boot_parameters(
122  ctx: RequestCtx,
123  Json(boot_params): Json<
124    ::manta_backend_dispatcher::types::bss::BootParameters,
125  >,
126) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
127  tracing::info!("add_boot_parameters");
128  let infra = ctx.infra();
129
130  service::boot_parameters::add_boot_parameters(
131    &infra,
132    &ctx.token,
133    &boot_params,
134  )
135  .await
136  .map_err(to_handler_error)?;
137
138  Ok((
139    StatusCode::CREATED,
140    Json(serde_json::json!({ "created": true })),
141  ))
142}
143
144// ---------------------------------------------------------------------------
145// PUT /api/v1/boot-parameters
146// ---------------------------------------------------------------------------
147
148/// PUT /boot-parameters — update boot image, kernel params, or runtime config for nodes.
149#[utoipa::path(put, path = "/boot-parameters", tag = "boot-parameters",
150  params(SiteHeader),
151  request_body = crate::service::boot_parameters::UpdateBootParametersParams,
152  security(("bearerAuth" = [])),
153  responses(
154    (status = 204, description = "Boot parameters updated"),
155    (status = 400, description = "Bad request",    body = ErrorResponse),
156    (status = 401, description = "Unauthorized",   body = ErrorResponse),
157    (status = 500, description = "Internal error", body = ErrorResponse),
158  )
159)]
160#[tracing::instrument(skip_all)]
161pub async fn update_boot_parameters(
162  ctx: RequestCtx,
163  Json(params): Json<service::boot_parameters::UpdateBootParametersParams>,
164) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
165  tracing::info!("update_boot_parameters");
166  let infra = ctx.infra();
167
168  service::boot_parameters::update_boot_parameters(&infra, &ctx.token, params)
169    .await
170    .map_err(to_handler_error)?;
171
172  Ok(StatusCode::NO_CONTENT)
173}
174
175// ---------------------------------------------------------------------------
176// POST /api/v1/boot-config — Apply boot configuration (with ?dry_run=true)
177// ---------------------------------------------------------------------------
178
179/// Request body for `POST /boot-config`.
180#[derive(Deserialize, ToSchema)]
181pub struct ApplyBootConfigRequest {
182  /// Node-set expression (xnames, HSM group, or nodeset) identifying the target nodes.
183  pub hosts_expression: String,
184  /// IMS image ID to set as the boot image.
185  pub boot_image_id: Option<String>,
186  /// CFS configuration name associated with the boot image.
187  pub boot_image_configuration: Option<String>,
188  /// Kernel command-line parameters to apply.
189  pub kernel_parameters: Option<String>,
190  /// CFS configuration to assign as the runtime desired-config.
191  pub runtime_configuration: Option<String>,
192  /// When true, returns the computed changeset without persisting it.
193  #[serde(default)]
194  pub dry_run: bool,
195}
196
197/// `POST /api/v1/boot-config` — apply BSS boot configuration to a set of nodes.
198#[utoipa::path(post, path = "/boot-config", tag = "boot-parameters",
199  params(SiteHeader),
200  request_body = ApplyBootConfigRequest,
201  security(("bearerAuth" = [])),
202  responses(
203    (status = 200, description = "Boot config applied or preview", body = serde_json::Value),
204    (status = 400, description = "Bad request",                    body = ErrorResponse),
205    (status = 401, description = "Unauthorized",                   body = ErrorResponse),
206    (status = 500, description = "Internal error",                 body = ErrorResponse),
207  )
208)]
209#[tracing::instrument(skip_all)]
210pub async fn apply_boot_config(
211  ctx: RequestCtx,
212  Json(body): Json<ApplyBootConfigRequest>,
213) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
214  tracing::info!(
215    "apply_boot_config hosts={} dry_run={}",
216    body.hosts_expression,
217    body.dry_run
218  );
219  let infra = ctx.infra();
220
221  let changeset = service::boot_parameters::prepare_boot_config(
222    &infra,
223    &ctx.token,
224    &body.hosts_expression,
225    body.boot_image_id.as_deref(),
226    body.boot_image_configuration.as_deref(),
227    body.kernel_parameters.as_deref(),
228  )
229  .await
230  .map_err(to_handler_error)?;
231
232  if body.dry_run {
233    return Ok((StatusCode::OK, Json(serialize_or_500(&changeset)?)));
234  }
235
236  service::boot_parameters::persist_boot_config(
237    &infra,
238    &ctx.token,
239    &changeset,
240    body.runtime_configuration.as_deref(),
241  )
242  .await
243  .map_err(to_handler_error)?;
244
245  Ok((
246    StatusCode::OK,
247    Json(serde_json::json!({
248      "applied": true,
249      "nodes": changeset.xname_vec,
250      "need_restart": changeset.need_restart,
251    })),
252  ))
253}