1use 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
23pub use manta_shared::types::api::queries::BootParametersQuery;
28
29#[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, ¶ms)
54 .await
55 .map_err(to_handler_error)?;
56
57 Ok(Json(boot_params))
58}
59
60#[derive(Deserialize, ToSchema)]
66pub struct DeleteBootParametersRequest {
67 pub hosts: Vec<String>,
69}
70
71#[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#[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#[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", ¶ms.hosts)
184 .detail("kernel", ¶ms.kernel)
185 .detail("initrd", ¶ms.initrd)
186 .detail("params", ¶ms.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
198pub use manta_shared::types::api::boot_parameters::ApplyBootConfigRequest;
203
204#[utoipa::path(post, path = "/boot-config", tag = "boot-parameters",
206 params(SiteHeader),
207 request_body = ApplyBootConfigRequest,
208 security(("bearerAuth" = [])),
209 responses(
210 (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}