Skip to main content

manta_server/server/handlers/
boot_parameters.rs

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