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(
260 infra: &InfraContext<'_>,
261 token: &str,
262 changeset: &BootConfigChangeset,
263 new_runtime_configuration_opt: Option<&str>,
264) -> Result<(), Error> {
265 tracing::info!("Persist changes");
266
267 validate_user_group_members_access(infra, token, &changeset.xname_vec)
268 .await?;
269
270 futures::future::join_all(changeset.boot_param_vec.iter().map(
273 |boot_parameter| async move {
274 tracing::debug!("Updating boot parameter:\n{:#?}", boot_parameter);
275 let component_patch_rep = infra
276 .backend
277 .update_bootparameters(token, boot_parameter)
278 .await;
279 tracing::debug!(
280 "Component boot parameters resp:\n{:#?}",
281 component_patch_rep
282 );
283 },
284 ))
285 .await;
286
287 if let Some(new_runtime_configuration_name) = new_runtime_configuration_opt {
288 tracing::info!(
289 "Updating runtime configuration to '{new_runtime_configuration_name}'"
290 );
291
292 infra
293 .backend
294 .update_runtime_configuration(
295 token,
296 &changeset.xname_vec,
297 new_runtime_configuration_name,
298 true,
299 )
300 .await?;
301
302 apply_image_patches(infra, token, &changeset.image_vec).await?;
303 } else {
304 tracing::info!("Runtime configuration does not change.");
305 }
306
307 Ok(())
308}
309
310async fn get_new_boot_image(
311 infra: &InfraContext<'_>,
312 shasta_token: &str,
313 new_boot_image_configuration_opt: Option<&str>,
314 new_boot_image_id_opt: Option<&str>,
315) -> Result<Option<Image>, Error> {
316 let new_boot_image = if let Some(new_boot_image_configuration) =
317 new_boot_image_configuration_opt
318 {
319 tracing::info!(
320 "Boot configuration '{}' provided",
321 new_boot_image_configuration
322 );
323 let mut image_vec = get_image_vec_related_cfs_configuration_name(
324 infra,
325 shasta_token,
326 new_boot_image_configuration.to_string(),
327 )
328 .await?;
329
330 if image_vec.is_empty() {
331 return Err(Error::NotFound(format!(
332 "No boot image found for configuration '{new_boot_image_configuration}'"
333 )));
334 }
335
336 infra.backend.filter_images(&mut image_vec)?;
337
338 let most_recent_image = image_vec.iter().last().ok_or_else(|| {
339 Error::NotFound("No image found for configuration".to_string())
340 })?;
341
342 tracing::debug!(
343 "Boot image id related to configuration '{}' found:\n{:#?}",
344 new_boot_image_configuration,
345 most_recent_image
346 );
347
348 Some(most_recent_image.clone())
349 } else if let Some(boot_image_id) = new_boot_image_id_opt {
350 tracing::info!("Boot image id '{}' provided", boot_image_id);
351 let image_in_csm_vec = infra
352 .backend
353 .get_images(shasta_token, new_boot_image_id_opt)
354 .await?;
355
356 if image_in_csm_vec.is_empty() {
357 return Err(Error::NotFound(format!(
358 "Boot image id '{boot_image_id}' not found"
359 )));
360 }
361
362 image_in_csm_vec.first().cloned()
363 } else {
364 None
365 };
366
367 Ok(new_boot_image)
368}
369
370fn apply_kernel_params(
371 boot_param_vec: &mut [BootParameters],
372 new_kernel_parameters: &str,
373) -> Result<bool, Error> {
374 tracing::info!(
380 "Updating kernel parameters to '{}' across {} boot-parameter record(s)",
381 new_kernel_parameters,
382 boot_param_vec.len()
383 );
384
385 let mut any_changed = false;
386
387 for boot_parameter in boot_param_vec.iter_mut() {
388 tracing::debug!(
389 "Updating '{:?}' kernel parameters to '{}'",
390 boot_parameter.hosts,
391 new_kernel_parameters
392 );
393
394 let changed = boot_parameter.apply_kernel_params(new_kernel_parameters);
395 any_changed = changed || any_changed;
396
397 let image_id = boot_parameter.try_get_boot_image_id().ok_or_else(|| {
398 Error::MissingField(format!(
399 "Could not get boot image id from boot parameters for hosts: {:?}",
400 boot_parameter.hosts
401 ))
402 })?;
403
404 boot_parameter
405 .update_boot_image(&image_id, &boot_parameter.get_boot_image_etag())?;
406 }
407
408 Ok(any_changed)
409}
410
411async fn collect_boot_images(
412 infra: &InfraContext<'_>,
413 shasta_token: &str,
414 boot_param_vec: &mut [BootParameters],
415 new_boot_image_opt: Option<Image>,
416 need_restart: &mut bool,
417) -> Result<HashMap<String, Image>, Error> {
418 let mut image_vec = HashMap::<String, Image>::new();
419
420 if let Some(new_boot_image) = new_boot_image_opt {
421 let new_boot_image_id = new_boot_image
422 .id
423 .as_ref()
424 .ok_or_else(|| {
425 Error::MissingField("New boot image id is missing".to_string())
426 })?
427 .clone();
428
429 let new_boot_image_etag = new_boot_image
430 .link
431 .as_ref()
432 .and_then(|link| link.etag.as_ref())
433 .ok_or_else(|| {
434 Error::MissingField("New boot image etag is missing".to_string())
435 })?;
436
437 image_vec.insert(new_boot_image_id.clone(), new_boot_image.clone());
438
439 let any_differ = boot_param_vec.iter().any(|bp| {
440 bp.try_get_boot_image_id().as_deref() != Some(new_boot_image_id.as_str())
441 });
442
443 if any_differ {
444 tracing::info!(
448 "Updating boot image to '{}' across {} boot-parameter record(s)",
449 new_boot_image_id,
450 boot_param_vec.len()
451 );
452 for boot_parameter in boot_param_vec.iter_mut() {
453 tracing::debug!(
454 "Updating '{:?}' boot image to '{}'",
455 boot_parameter.hosts,
456 new_boot_image_id
457 );
458 boot_parameter
459 .update_boot_image(&new_boot_image_id, new_boot_image_etag)?;
460 }
461 *need_restart = true;
462 }
463 } else {
464 let mut unique_ids: Vec<String> = Vec::new();
469 let mut seen: std::collections::HashSet<String> =
470 std::collections::HashSet::new();
471 for boot_parameter in boot_param_vec.iter() {
472 let boot_image_id =
473 boot_parameter.try_get_boot_image_id().ok_or_else(|| {
474 Error::MissingField(format!(
475 "Could not get boot image id from boot parameters for hosts: {:?}",
476 boot_parameter.hosts
477 ))
478 })?;
479 if seen.insert(boot_image_id.clone()) {
480 unique_ids.push(boot_image_id);
481 }
482 }
483
484 let fetched: Vec<(String, Image)> =
485 futures::future::try_join_all(unique_ids.iter().map(|id| async move {
486 let image = infra
487 .backend
488 .get_images(shasta_token, Some(id.as_str()))
489 .await?
490 .first()
491 .ok_or_else(|| {
492 Error::NotFound(format!("No image found for boot image id '{id}'"))
493 })?
494 .clone();
495 Ok::<_, Error>((id.clone(), image))
496 }))
497 .await?;
498
499 for (id, image) in fetched {
500 image_vec.insert(id, image);
501 }
502 }
503
504 Ok(image_vec)
505}
506
507pub fn get_restricted_boot_parameters(
512 group_available_vec: &[Group],
513 boot_parameter_vec: &[BootParameters],
514) -> Vec<BootParameters> {
515 let member_set: HashSet<String> = group_available_vec
519 .iter()
520 .flat_map(Group::get_members)
521 .collect();
522
523 boot_parameter_vec
524 .iter()
525 .filter(|boot_param| {
526 boot_param
527 .hosts
528 .iter()
529 .any(|h| member_set.contains(h.as_str()))
530 })
531 .cloned()
532 .collect::<Vec<BootParameters>>()
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538 use manta_backend_dispatcher::types::Member;
539
540 fn make_group(label: &str, member_ids: Vec<&str>) -> Group {
542 Group {
543 label: label.to_string(),
544 description: None,
545 tags: None,
546 members: Some(Member {
547 ids: Some(member_ids.into_iter().map(String::from).collect()),
548 }),
549 exclusive_group: None,
550 }
551 }
552
553 fn make_boot_params(hosts: Vec<&str>) -> BootParameters {
555 BootParameters {
556 hosts: hosts.into_iter().map(String::from).collect(),
557 ..Default::default()
558 }
559 }
560
561 #[test]
562 fn filters_boot_params_by_group_membership() {
563 let groups =
564 vec![make_group("grp1", vec!["x1000c0s0b0n0", "x1000c0s0b0n1"])];
565 let boot_params = vec![
566 make_boot_params(vec!["x1000c0s0b0n0"]),
567 make_boot_params(vec!["x9999c0s0b0n0"]),
568 make_boot_params(vec!["x1000c0s0b0n1"]),
569 ];
570 let result = get_restricted_boot_parameters(&groups, &boot_params);
571 assert_eq!(result.len(), 2);
572 assert_eq!(result[0].hosts, vec!["x1000c0s0b0n0"]);
573 assert_eq!(result[1].hosts, vec!["x1000c0s0b0n1"]);
574 }
575
576 #[test]
577 fn returns_empty_when_no_group_members_match() {
578 let groups = vec![make_group("grp1", vec!["x1000c0s0b0n0"])];
579 let boot_params = vec![make_boot_params(vec!["x9999c0s0b0n0"])];
580 let result = get_restricted_boot_parameters(&groups, &boot_params);
581 assert!(result.is_empty());
582 }
583
584 #[test]
585 fn returns_empty_when_groups_are_empty() {
586 let boot_params = vec![make_boot_params(vec!["x1000c0s0b0n0"])];
587 let result = get_restricted_boot_parameters(&[], &boot_params);
588 assert!(result.is_empty());
589 }
590
591 #[test]
592 fn returns_empty_when_boot_params_are_empty() {
593 let groups = vec![make_group("grp1", vec!["x1000c0s0b0n0"])];
594 let result = get_restricted_boot_parameters(&groups, &[]);
595 assert!(result.is_empty());
596 }
597
598 #[test]
599 fn aggregates_members_across_multiple_groups() {
600 let groups = vec![
601 make_group("grp1", vec!["x1000c0s0b0n0"]),
602 make_group("grp2", vec!["x2000c0s0b0n0"]),
603 ];
604 let boot_params = vec![
605 make_boot_params(vec!["x1000c0s0b0n0"]),
606 make_boot_params(vec!["x2000c0s0b0n0"]),
607 make_boot_params(vec!["x3000c0s0b0n0"]),
608 ];
609 let result = get_restricted_boot_parameters(&groups, &boot_params);
610 assert_eq!(result.len(), 2);
611 }
612}