1use manta_backend_dispatcher::{
16 error::Error,
17 interfaces::{bss::BootParametersTrait, cfs::CfsTrait, ims::ImsTrait},
18 types::{Group, bss::BootParameters, ims::Image},
19};
20use std::collections::{HashMap, HashSet};
21
22use crate::server::common::app_context::InfraContext;
23use crate::service::authorization::validate_user_group_members_access;
24use crate::service::ims_ops::{
25 apply_image_patches, get_image_vec_related_cfs_configuration_name,
26};
27use crate::service::node_ops;
28pub use manta_shared::types::api::boot_parameters::{
29 GetBootParametersParams, UpdateBootParametersParams,
30};
31
32pub async fn get_boot_parameters(
50 infra: &InfraContext<'_>,
51 token: &str,
52 params: &GetBootParametersParams,
53) -> Result<Vec<BootParameters>, Error> {
54 tracing::info!("Get boot parameters");
55
56 let xname_vec = node_ops::resolve_target_nodes(
57 infra,
58 token,
59 params.host_expression.as_deref(),
60 params.group_name.as_deref(),
61 params.settings_group_name.as_deref(),
62 )
63 .await?;
64
65 if xname_vec.is_empty() {
66 return Err(Error::BadRequest(
67 "The list of nodes to operate is empty. Nothing to do".to_string(),
68 ));
69 }
70
71 validate_user_group_members_access(infra, token, &xname_vec).await?;
72
73 infra.backend.get_bootparameters(token, &xname_vec).await
74}
75
76pub async fn delete_boot_parameters(
83 infra: &InfraContext<'_>,
84 token: &str,
85 hosts: Vec<String>,
86) -> Result<(), Error> {
87 let boot_parameters = BootParameters {
88 hosts,
89 macs: None,
90 nids: None,
91 params: String::new(),
92 kernel: String::new(),
93 initrd: String::new(),
94 cloud_init: None,
95 };
96
97 validate_user_group_members_access(infra, token, &boot_parameters.hosts)
98 .await?;
99
100 infra
101 .backend
102 .delete_bootparameters(token, &boot_parameters)
103 .await
104 .map(|_| ())
105}
106
107pub async fn add_boot_parameters(
113 infra: &InfraContext<'_>,
114 token: &str,
115 boot_parameters: &BootParameters,
116) -> Result<(), Error> {
117 validate_user_group_members_access(infra, token, &boot_parameters.hosts)
118 .await?;
119
120 infra
121 .backend
122 .add_bootparameters(token, boot_parameters)
123 .await
124}
125
126pub async fn update_boot_parameters(
133 infra: &InfraContext<'_>,
134 token: &str,
135 params: UpdateBootParametersParams,
136) -> Result<(), Error> {
137 validate_user_group_members_access(infra, token, ¶ms.hosts).await?;
138
139 let boot_parameters = BootParameters {
140 hosts: params.hosts,
141 macs: params.macs,
142 nids: params.nids,
143 params: params.params,
144 kernel: params.kernel,
145 initrd: params.initrd,
146 cloud_init: None,
147 };
148
149 tracing::debug!("new boot params: {:#?}", boot_parameters);
150
151 infra
152 .backend
153 .update_bootparameters(token, &boot_parameters)
154 .await
155}
156
157#[derive(serde::Serialize)]
159pub(crate) struct BootConfigChangeset {
160 pub xname_vec: Vec<String>,
162 pub boot_param_vec: Vec<BootParameters>,
164 pub image_vec: HashMap<String, Image>,
166 pub need_restart: bool,
168}
169
170pub(crate) async fn prepare_boot_config(
183 infra: &InfraContext<'_>,
184 token: &str,
185 hosts_expression: &str,
186 new_boot_image_id_opt: Option<&str>,
187 new_boot_image_configuration_opt: Option<&str>,
188 new_kernel_parameters_opt: Option<&str>,
189) -> Result<BootConfigChangeset, Error> {
190 let mut need_restart = false;
191
192 let xname_vec = node_ops::from_user_hosts_expression_to_xname_vec(
193 infra,
194 token,
195 hosts_expression,
196 false,
197 )
198 .await?;
199
200 validate_user_group_members_access(infra, token, &xname_vec).await?;
205
206 let mut current_node_boot_param_vec: Vec<BootParameters> =
207 infra.backend.get_bootparameters(token, &xname_vec).await?;
208
209 let new_boot_image_opt = get_new_boot_image(
210 infra,
211 token,
212 new_boot_image_configuration_opt,
213 new_boot_image_id_opt,
214 )
215 .await?;
216
217 if let Some(new_kernel_parameters) = new_kernel_parameters_opt {
219 need_restart |= apply_kernel_params(
220 &mut current_node_boot_param_vec,
221 new_kernel_parameters,
222 )?;
223 }
224
225 let mut image_vec = collect_boot_images(
226 infra,
227 token,
228 &mut current_node_boot_param_vec,
229 new_boot_image_opt,
230 &mut need_restart,
231 )
232 .await?;
233
234 if current_node_boot_param_vec
235 .first()
236 .ok_or_else(|| Error::NotFound("No boot parameters found".to_string()))?
237 .is_root_kernel_param_iscsi_ready()
238 {
239 for image in image_vec.values_mut() {
240 image.set_boot_image_iscsi_ready();
241 }
242 }
243
244 Ok(BootConfigChangeset {
245 xname_vec,
246 boot_param_vec: current_node_boot_param_vec,
247 image_vec,
248 need_restart,
249 })
250}
251
252pub(crate) async fn persist_boot_config(
266 infra: &InfraContext<'_>,
267 token: &str,
268 changeset: &BootConfigChangeset,
269 new_runtime_configuration_opt: Option<&str>,
270 enabled_opt: Option<bool>,
271) -> Result<(), Error> {
272 tracing::info!("Persist changes");
273
274 validate_user_group_members_access(infra, token, &changeset.xname_vec)
275 .await?;
276
277 futures::future::join_all(changeset.boot_param_vec.iter().map(
280 |boot_parameter| async move {
281 tracing::debug!("Updating boot parameter:\n{:#?}", boot_parameter);
282 let component_patch_rep = infra
283 .backend
284 .update_bootparameters(token, boot_parameter)
285 .await;
286 tracing::debug!(
287 "Component boot parameters resp:\n{:#?}",
288 component_patch_rep
289 );
290 },
291 ))
292 .await;
293
294 if let Some(new_runtime_configuration_name) = new_runtime_configuration_opt {
295 tracing::info!(
296 "Updating runtime configuration to '{new_runtime_configuration_name}'"
297 );
298
299 infra
300 .backend
301 .update_runtime_configuration(
302 token,
303 &changeset.xname_vec,
304 new_runtime_configuration_name,
305 enabled_opt.unwrap_or(true),
306 )
307 .await?;
308
309 apply_image_patches(infra, token, &changeset.image_vec).await?;
310 } else {
311 tracing::info!("Runtime configuration does not change.");
312 }
313
314 Ok(())
315}
316
317async fn get_new_boot_image(
318 infra: &InfraContext<'_>,
319 shasta_token: &str,
320 new_boot_image_configuration_opt: Option<&str>,
321 new_boot_image_id_opt: Option<&str>,
322) -> Result<Option<Image>, Error> {
323 let new_boot_image = if let Some(new_boot_image_configuration) =
324 new_boot_image_configuration_opt
325 {
326 tracing::info!(
327 "Boot configuration '{}' provided",
328 new_boot_image_configuration
329 );
330 let mut image_vec = get_image_vec_related_cfs_configuration_name(
331 infra,
332 shasta_token,
333 new_boot_image_configuration.to_string(),
334 )
335 .await?;
336
337 if image_vec.is_empty() {
338 return Err(Error::NotFound(format!(
339 "No boot image found for configuration '{new_boot_image_configuration}'"
340 )));
341 }
342
343 infra.backend.filter_images(&mut image_vec)?;
344
345 let most_recent_image = image_vec.iter().last().ok_or_else(|| {
346 Error::NotFound("No image found for configuration".to_string())
347 })?;
348
349 tracing::debug!(
350 "Boot image id related to configuration '{}' found:\n{:#?}",
351 new_boot_image_configuration,
352 most_recent_image
353 );
354
355 Some(most_recent_image.clone())
356 } else if let Some(boot_image_id) = new_boot_image_id_opt {
357 tracing::info!("Boot image id '{}' provided", boot_image_id);
358 let image_in_csm_vec = infra
359 .backend
360 .get_images(shasta_token, new_boot_image_id_opt)
361 .await?;
362
363 if image_in_csm_vec.is_empty() {
364 return Err(Error::NotFound(format!(
365 "Boot image id '{boot_image_id}' not found"
366 )));
367 }
368
369 image_in_csm_vec.first().cloned()
370 } else {
371 None
372 };
373
374 Ok(new_boot_image)
375}
376
377fn apply_kernel_params(
378 boot_param_vec: &mut [BootParameters],
379 new_kernel_parameters: &str,
380) -> Result<bool, Error> {
381 tracing::info!(
387 "Updating kernel parameters to '{}' across {} boot-parameter record(s)",
388 new_kernel_parameters,
389 boot_param_vec.len()
390 );
391
392 let mut any_changed = false;
393
394 for boot_parameter in boot_param_vec.iter_mut() {
395 tracing::debug!(
396 "Updating '{:?}' kernel parameters to '{}'",
397 boot_parameter.hosts,
398 new_kernel_parameters
399 );
400
401 let changed = boot_parameter.apply_kernel_params(new_kernel_parameters);
402 any_changed = changed || any_changed;
403
404 let image_id = boot_parameter.try_get_boot_image_id().ok_or_else(|| {
405 Error::MissingField(format!(
406 "Could not get boot image id from boot parameters for hosts: {:?}",
407 boot_parameter.hosts
408 ))
409 })?;
410
411 boot_parameter
412 .update_boot_image(&image_id, &boot_parameter.get_boot_image_etag())?;
413 }
414
415 Ok(any_changed)
416}
417
418async fn collect_boot_images(
419 infra: &InfraContext<'_>,
420 shasta_token: &str,
421 boot_param_vec: &mut [BootParameters],
422 new_boot_image_opt: Option<Image>,
423 need_restart: &mut bool,
424) -> Result<HashMap<String, Image>, Error> {
425 let mut image_vec = HashMap::<String, Image>::new();
426
427 if let Some(new_boot_image) = new_boot_image_opt {
428 let new_boot_image_id = new_boot_image
429 .id
430 .as_ref()
431 .ok_or_else(|| {
432 Error::MissingField("New boot image id is missing".to_string())
433 })?
434 .clone();
435
436 let new_boot_image_etag = new_boot_image
437 .link
438 .as_ref()
439 .and_then(|link| link.etag.as_ref())
440 .ok_or_else(|| {
441 Error::MissingField("New boot image etag is missing".to_string())
442 })?;
443
444 image_vec.insert(new_boot_image_id.clone(), new_boot_image.clone());
445
446 let any_differ = boot_param_vec.iter().any(|bp| {
447 bp.try_get_boot_image_id().as_deref() != Some(new_boot_image_id.as_str())
448 });
449
450 if any_differ {
451 tracing::info!(
455 "Updating boot image to '{}' across {} boot-parameter record(s)",
456 new_boot_image_id,
457 boot_param_vec.len()
458 );
459 for boot_parameter in boot_param_vec.iter_mut() {
460 tracing::debug!(
461 "Updating '{:?}' boot image to '{}'",
462 boot_parameter.hosts,
463 new_boot_image_id
464 );
465 boot_parameter
466 .update_boot_image(&new_boot_image_id, new_boot_image_etag)?;
467 }
468 *need_restart = true;
469 }
470 } else {
471 let mut unique_ids: Vec<String> = Vec::new();
476 let mut seen: std::collections::HashSet<String> =
477 std::collections::HashSet::new();
478 for boot_parameter in boot_param_vec.iter() {
479 let boot_image_id =
480 boot_parameter.try_get_boot_image_id().ok_or_else(|| {
481 Error::MissingField(format!(
482 "Could not get boot image id from boot parameters for hosts: {:?}",
483 boot_parameter.hosts
484 ))
485 })?;
486 if seen.insert(boot_image_id.clone()) {
487 unique_ids.push(boot_image_id);
488 }
489 }
490
491 let fetched: Vec<(String, Image)> =
492 futures::future::try_join_all(unique_ids.iter().map(|id| async move {
493 let image = infra
494 .backend
495 .get_images(shasta_token, Some(id.as_str()))
496 .await?
497 .first()
498 .ok_or_else(|| {
499 Error::NotFound(format!("No image found for boot image id '{id}'"))
500 })?
501 .clone();
502 Ok::<_, Error>((id.clone(), image))
503 }))
504 .await?;
505
506 for (id, image) in fetched {
507 image_vec.insert(id, image);
508 }
509 }
510
511 Ok(image_vec)
512}
513
514pub fn get_restricted_boot_parameters(
519 group_available_vec: &[Group],
520 boot_parameter_vec: &[BootParameters],
521) -> Vec<BootParameters> {
522 let member_set: HashSet<String> = group_available_vec
526 .iter()
527 .flat_map(Group::get_members)
528 .collect();
529
530 boot_parameter_vec
531 .iter()
532 .filter(|boot_param| {
533 boot_param
534 .hosts
535 .iter()
536 .any(|h| member_set.contains(h.as_str()))
537 })
538 .cloned()
539 .collect::<Vec<BootParameters>>()
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545 use manta_backend_dispatcher::types::Member;
546
547 fn make_group(label: &str, member_ids: Vec<&str>) -> Group {
549 Group {
550 label: label.to_string(),
551 description: None,
552 tags: None,
553 members: Some(Member {
554 ids: Some(member_ids.into_iter().map(String::from).collect()),
555 }),
556 exclusive_group: None,
557 }
558 }
559
560 fn make_boot_params(hosts: Vec<&str>) -> BootParameters {
562 BootParameters {
563 hosts: hosts.into_iter().map(String::from).collect(),
564 ..Default::default()
565 }
566 }
567
568 #[test]
569 fn filters_boot_params_by_group_membership() {
570 let groups =
571 vec![make_group("grp1", vec!["x1000c0s0b0n0", "x1000c0s0b0n1"])];
572 let boot_params = vec![
573 make_boot_params(vec!["x1000c0s0b0n0"]),
574 make_boot_params(vec!["x9999c0s0b0n0"]),
575 make_boot_params(vec!["x1000c0s0b0n1"]),
576 ];
577 let result = get_restricted_boot_parameters(&groups, &boot_params);
578 assert_eq!(result.len(), 2);
579 assert_eq!(result[0].hosts, vec!["x1000c0s0b0n0"]);
580 assert_eq!(result[1].hosts, vec!["x1000c0s0b0n1"]);
581 }
582
583 #[test]
584 fn returns_empty_when_no_group_members_match() {
585 let groups = vec![make_group("grp1", vec!["x1000c0s0b0n0"])];
586 let boot_params = vec![make_boot_params(vec!["x9999c0s0b0n0"])];
587 let result = get_restricted_boot_parameters(&groups, &boot_params);
588 assert!(result.is_empty());
589 }
590
591 #[test]
592 fn returns_empty_when_groups_are_empty() {
593 let boot_params = vec![make_boot_params(vec!["x1000c0s0b0n0"])];
594 let result = get_restricted_boot_parameters(&[], &boot_params);
595 assert!(result.is_empty());
596 }
597
598 #[test]
599 fn returns_empty_when_boot_params_are_empty() {
600 let groups = vec![make_group("grp1", vec!["x1000c0s0b0n0"])];
601 let result = get_restricted_boot_parameters(&groups, &[]);
602 assert!(result.is_empty());
603 }
604
605 #[test]
606 fn aggregates_members_across_multiple_groups() {
607 let groups = vec![
608 make_group("grp1", vec!["x1000c0s0b0n0"]),
609 make_group("grp2", vec!["x2000c0s0b0n0"]),
610 ];
611 let boot_params = vec![
612 make_boot_params(vec!["x1000c0s0b0n0"]),
613 make_boot_params(vec!["x2000c0s0b0n0"]),
614 make_boot_params(vec!["x3000c0s0b0n0"]),
615 ];
616 let result = get_restricted_boot_parameters(&groups, &boot_params);
617 assert_eq!(result.len(), 2);
618 }
619}