1use manta_backend_dispatcher::{
4 error::Error,
5 interfaces::{bss::BootParametersTrait, cfs::CfsTrait, ims::ImsTrait},
6 types::{
7 Group,
8 bss::BootParameters,
9 ims::{Image, PatchImage},
10 },
11};
12use std::collections::HashMap;
13
14use crate::server::common::app_context::InfraContext;
15use crate::service::authorization::validate_user_group_members_access;
16use crate::service::ims_ops::get_image_vec_related_cfs_configuration_name;
17use crate::service::node_ops;
18pub use manta_shared::types::api::boot_parameters::{
19 GetBootParametersParams, UpdateBootParametersParams,
20};
21
22pub async fn get_boot_parameters(
32 infra: &InfraContext<'_>,
33 token: &str,
34 params: &GetBootParametersParams,
35) -> Result<Vec<BootParameters>, Error> {
36 tracing::info!("Get boot parameters");
37
38 let xname_vec = node_ops::resolve_target_nodes(
39 infra,
40 token,
41 params.host_expression.as_deref(),
42 params.group_name.as_deref(),
43 params.settings_group_name.as_deref(),
44 )
45 .await?;
46
47 validate_user_group_members_access(infra, token, &xname_vec).await?;
48
49 if xname_vec.is_empty() {
50 return Err(Error::BadRequest(
51 "The list of nodes to operate is empty. Nothing to do".to_string(),
52 ));
53 }
54
55 validate_user_group_members_access(infra, token, &xname_vec).await?;
56
57 infra.backend.get_bootparameters(token, &xname_vec).await
58}
59
60pub async fn delete_boot_parameters(
67 infra: &InfraContext<'_>,
68 token: &str,
69 hosts: Vec<String>,
70) -> Result<(), Error> {
71 let boot_parameters = BootParameters {
72 hosts,
73 macs: None,
74 nids: None,
75 params: String::new(),
76 kernel: String::new(),
77 initrd: String::new(),
78 cloud_init: None,
79 };
80
81 validate_user_group_members_access(infra, token, &boot_parameters.hosts)
82 .await?;
83
84 infra
85 .backend
86 .delete_bootparameters(token, &boot_parameters)
87 .await
88 .map(|_| ())
89}
90
91pub async fn add_boot_parameters(
97 infra: &InfraContext<'_>,
98 token: &str,
99 boot_parameters: &BootParameters,
100) -> Result<(), Error> {
101 validate_user_group_members_access(infra, token, &boot_parameters.hosts)
102 .await?;
103
104 infra
105 .backend
106 .add_bootparameters(token, boot_parameters)
107 .await
108}
109
110pub async fn update_boot_parameters(
117 infra: &InfraContext<'_>,
118 token: &str,
119 params: UpdateBootParametersParams,
120) -> Result<(), Error> {
121 validate_user_group_members_access(infra, token, ¶ms.hosts).await?;
122
123 let boot_parameters = BootParameters {
124 hosts: params.hosts,
125 macs: params.macs,
126 nids: params.nids,
127 params: params.params,
128 kernel: params.kernel,
129 initrd: params.initrd,
130 cloud_init: None,
131 };
132
133 tracing::debug!("new boot params: {:#?}", boot_parameters);
134
135 infra
136 .backend
137 .update_bootparameters(token, &boot_parameters)
138 .await
139}
140
141#[derive(serde::Serialize)]
143pub(crate) struct BootConfigChangeset {
144 pub xname_vec: Vec<String>,
146 pub boot_param_vec: Vec<BootParameters>,
148 pub image_vec: HashMap<String, Image>,
150 pub need_restart: bool,
152}
153
154pub(crate) async fn prepare_boot_config(
167 infra: &InfraContext<'_>,
168 token: &str,
169 hosts_expression: &str,
170 new_boot_image_id_opt: Option<&str>,
171 new_boot_image_configuration_opt: Option<&str>,
172 new_kernel_parameters_opt: Option<&str>,
173) -> Result<BootConfigChangeset, Error> {
174 let mut need_restart = false;
175
176 let xname_vec = node_ops::from_user_hosts_expression_to_xname_vec(
177 infra,
178 token,
179 hosts_expression,
180 false,
181 )
182 .await?;
183
184 validate_user_group_members_access(infra, token, &xname_vec).await?;
189
190 let mut current_node_boot_param_vec: Vec<BootParameters> =
191 infra.backend.get_bootparameters(token, &xname_vec).await?;
192
193 let new_boot_image_opt = get_new_boot_image(
194 infra,
195 token,
196 new_boot_image_configuration_opt,
197 new_boot_image_id_opt,
198 )
199 .await?;
200
201 if let Some(new_kernel_parameters) = new_kernel_parameters_opt {
203 need_restart |= apply_kernel_params(
204 &mut current_node_boot_param_vec,
205 new_kernel_parameters,
206 )?;
207 }
208
209 let mut image_vec = collect_boot_images(
210 infra,
211 token,
212 &mut current_node_boot_param_vec,
213 new_boot_image_opt,
214 &mut need_restart,
215 )
216 .await?;
217
218 if current_node_boot_param_vec
219 .first()
220 .ok_or_else(|| Error::NotFound("No boot parameters found".to_string()))?
221 .is_root_kernel_param_iscsi_ready()
222 {
223 for image in image_vec.values_mut() {
224 image.set_boot_image_iscsi_ready();
225 }
226 }
227
228 Ok(BootConfigChangeset {
229 xname_vec,
230 boot_param_vec: current_node_boot_param_vec,
231 image_vec,
232 need_restart,
233 })
234}
235
236pub(crate) async fn persist_boot_config(
244 infra: &InfraContext<'_>,
245 token: &str,
246 changeset: &BootConfigChangeset,
247 new_runtime_configuration_opt: Option<&str>,
248) -> Result<(), Error> {
249 tracing::info!("Persist changes");
250
251 validate_user_group_members_access(infra, token, &changeset.xname_vec)
252 .await?;
253
254 for boot_parameter in &changeset.boot_param_vec {
255 tracing::debug!("Updating boot parameter:\n{:#?}", boot_parameter);
256 let component_patch_rep = infra
257 .backend
258 .update_bootparameters(token, boot_parameter)
259 .await;
260 tracing::debug!(
261 "Component boot parameters resp:\n{:#?}",
262 component_patch_rep
263 );
264 }
265
266 if let Some(new_runtime_configuration_name) = new_runtime_configuration_opt {
267 tracing::info!(
268 "Updating runtime configuration to '{new_runtime_configuration_name}'"
269 );
270
271 infra
272 .backend
273 .update_runtime_configuration(
274 token,
275 &changeset.xname_vec,
276 new_runtime_configuration_name,
277 true,
278 )
279 .await?;
280
281 for image in changeset.image_vec.values() {
282 let image_id = image.id.clone().ok_or_else(|| {
283 Error::MissingField("Image id is missing".to_string())
284 })?;
285 let patch_image: PatchImage = image.clone().into();
286 infra
287 .backend
288 .update_image(token, &image_id, &patch_image)
289 .await?;
290 }
291 } else {
292 tracing::info!("Runtime configuration does not change.");
293 }
294
295 Ok(())
296}
297
298async fn get_new_boot_image(
299 infra: &InfraContext<'_>,
300 shasta_token: &str,
301 new_boot_image_configuration_opt: Option<&str>,
302 new_boot_image_id_opt: Option<&str>,
303) -> Result<Option<Image>, Error> {
304 let new_boot_image = if let Some(new_boot_image_configuration) =
305 new_boot_image_configuration_opt
306 {
307 tracing::info!(
308 "Boot configuration '{}' provided",
309 new_boot_image_configuration
310 );
311 let mut image_vec = get_image_vec_related_cfs_configuration_name(
312 infra,
313 shasta_token,
314 new_boot_image_configuration.to_string(),
315 )
316 .await?;
317
318 if image_vec.is_empty() {
319 return Err(Error::NotFound(format!(
320 "No boot image found for configuration '{new_boot_image_configuration}'"
321 )));
322 }
323
324 infra.backend.filter_images(&mut image_vec)?;
325
326 let most_recent_image = image_vec.iter().last().ok_or_else(|| {
327 Error::NotFound("No image found for configuration".to_string())
328 })?;
329
330 tracing::debug!(
331 "Boot image id related to configuration '{}' found:\n{:#?}",
332 new_boot_image_configuration,
333 most_recent_image
334 );
335
336 Some(most_recent_image.clone())
337 } else if let Some(boot_image_id) = new_boot_image_id_opt {
338 tracing::info!("Boot image id '{}' provided", boot_image_id);
339 let image_in_csm_vec = infra
340 .backend
341 .get_images(shasta_token, new_boot_image_id_opt)
342 .await?;
343
344 if image_in_csm_vec.is_empty() {
345 return Err(Error::NotFound(format!(
346 "Boot image id '{boot_image_id}' not found"
347 )));
348 }
349
350 image_in_csm_vec.first().cloned()
351 } else {
352 None
353 };
354
355 Ok(new_boot_image)
356}
357
358fn apply_kernel_params(
359 boot_param_vec: &mut [BootParameters],
360 new_kernel_parameters: &str,
361) -> Result<bool, Error> {
362 tracing::info!(
368 "Updating kernel parameters to '{}' across {} boot-parameter record(s)",
369 new_kernel_parameters,
370 boot_param_vec.len()
371 );
372
373 let mut any_changed = false;
374
375 for boot_parameter in boot_param_vec.iter_mut() {
376 tracing::debug!(
377 "Updating '{:?}' kernel parameters to '{}'",
378 boot_parameter.hosts,
379 new_kernel_parameters
380 );
381
382 let changed = boot_parameter.apply_kernel_params(new_kernel_parameters);
383 any_changed = changed || any_changed;
384
385 let image_id = boot_parameter.try_get_boot_image_id().ok_or_else(|| {
386 Error::MissingField(format!(
387 "Could not get boot image id from boot parameters for hosts: {:?}",
388 boot_parameter.hosts
389 ))
390 })?;
391
392 boot_parameter
393 .update_boot_image(&image_id, &boot_parameter.get_boot_image_etag())?;
394 }
395
396 Ok(any_changed)
397}
398
399async fn collect_boot_images(
400 infra: &InfraContext<'_>,
401 shasta_token: &str,
402 boot_param_vec: &mut [BootParameters],
403 new_boot_image_opt: Option<Image>,
404 need_restart: &mut bool,
405) -> Result<HashMap<String, Image>, Error> {
406 let mut image_vec = HashMap::<String, Image>::new();
407
408 if let Some(new_boot_image) = new_boot_image_opt {
409 let new_boot_image_id = new_boot_image
410 .id
411 .as_ref()
412 .ok_or_else(|| {
413 Error::MissingField("New boot image id is missing".to_string())
414 })?
415 .clone();
416
417 let new_boot_image_etag = new_boot_image
418 .link
419 .as_ref()
420 .and_then(|link| link.etag.as_ref())
421 .ok_or_else(|| {
422 Error::MissingField("New boot image etag is missing".to_string())
423 })?;
424
425 image_vec.insert(new_boot_image_id.clone(), new_boot_image.clone());
426
427 let any_differ = boot_param_vec.iter().any(|bp| {
428 bp.try_get_boot_image_id().as_deref() != Some(new_boot_image_id.as_str())
429 });
430
431 if any_differ {
432 tracing::info!(
436 "Updating boot image to '{}' across {} boot-parameter record(s)",
437 new_boot_image_id,
438 boot_param_vec.len()
439 );
440 for boot_parameter in boot_param_vec.iter_mut() {
441 tracing::debug!(
442 "Updating '{:?}' boot image to '{}'",
443 boot_parameter.hosts,
444 new_boot_image_id
445 );
446 boot_parameter
447 .update_boot_image(&new_boot_image_id, new_boot_image_etag)?;
448 }
449 *need_restart = true;
450 }
451 } else {
452 let mut unique_ids: Vec<String> = Vec::new();
457 let mut seen: std::collections::HashSet<String> =
458 std::collections::HashSet::new();
459 for boot_parameter in boot_param_vec.iter() {
460 let boot_image_id =
461 boot_parameter.try_get_boot_image_id().ok_or_else(|| {
462 Error::MissingField(format!(
463 "Could not get boot image id from boot parameters for hosts: {:?}",
464 boot_parameter.hosts
465 ))
466 })?;
467 if seen.insert(boot_image_id.clone()) {
468 unique_ids.push(boot_image_id);
469 }
470 }
471
472 let fetched: Vec<(String, Image)> =
473 futures::future::try_join_all(unique_ids.iter().map(|id| async move {
474 let image = infra
475 .backend
476 .get_images(shasta_token, Some(id.as_str()))
477 .await?
478 .first()
479 .ok_or_else(|| {
480 Error::NotFound(format!("No image found for boot image id '{id}'"))
481 })?
482 .clone();
483 Ok::<_, Error>((id.clone(), image))
484 }))
485 .await?;
486
487 for (id, image) in fetched {
488 image_vec.insert(id, image);
489 }
490 }
491
492 Ok(image_vec)
493}
494
495pub fn get_restricted_boot_parameters(
500 group_available_vec: &[Group],
501 boot_parameter_vec: &[BootParameters],
502) -> Vec<BootParameters> {
503 let group_members: Vec<String> = group_available_vec
504 .iter()
505 .flat_map(Group::get_members)
506 .collect();
507
508 boot_parameter_vec
509 .iter()
510 .filter(|boot_param| {
511 group_members
512 .iter()
513 .any(|gma| boot_param.hosts.contains(gma))
514 })
515 .cloned()
516 .collect::<Vec<BootParameters>>()
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522 use manta_backend_dispatcher::types::Member;
523
524 fn make_group(label: &str, member_ids: Vec<&str>) -> Group {
526 Group {
527 label: label.to_string(),
528 description: None,
529 tags: None,
530 members: Some(Member {
531 ids: Some(member_ids.into_iter().map(String::from).collect()),
532 }),
533 exclusive_group: None,
534 }
535 }
536
537 fn make_boot_params(hosts: Vec<&str>) -> BootParameters {
539 BootParameters {
540 hosts: hosts.into_iter().map(String::from).collect(),
541 ..Default::default()
542 }
543 }
544
545 #[test]
546 fn filters_boot_params_by_group_membership() {
547 let groups =
548 vec![make_group("grp1", vec!["x1000c0s0b0n0", "x1000c0s0b0n1"])];
549 let boot_params = vec![
550 make_boot_params(vec!["x1000c0s0b0n0"]),
551 make_boot_params(vec!["x9999c0s0b0n0"]),
552 make_boot_params(vec!["x1000c0s0b0n1"]),
553 ];
554 let result = get_restricted_boot_parameters(&groups, &boot_params);
555 assert_eq!(result.len(), 2);
556 assert_eq!(result[0].hosts, vec!["x1000c0s0b0n0"]);
557 assert_eq!(result[1].hosts, vec!["x1000c0s0b0n1"]);
558 }
559
560 #[test]
561 fn returns_empty_when_no_group_members_match() {
562 let groups = vec![make_group("grp1", vec!["x1000c0s0b0n0"])];
563 let boot_params = vec![make_boot_params(vec!["x9999c0s0b0n0"])];
564 let result = get_restricted_boot_parameters(&groups, &boot_params);
565 assert!(result.is_empty());
566 }
567
568 #[test]
569 fn returns_empty_when_groups_are_empty() {
570 let boot_params = vec![make_boot_params(vec!["x1000c0s0b0n0"])];
571 let result = get_restricted_boot_parameters(&[], &boot_params);
572 assert!(result.is_empty());
573 }
574
575 #[test]
576 fn returns_empty_when_boot_params_are_empty() {
577 let groups = vec![make_group("grp1", vec!["x1000c0s0b0n0"])];
578 let result = get_restricted_boot_parameters(&groups, &[]);
579 assert!(result.is_empty());
580 }
581
582 #[test]
583 fn aggregates_members_across_multiple_groups() {
584 let groups = vec![
585 make_group("grp1", vec!["x1000c0s0b0n0"]),
586 make_group("grp2", vec!["x2000c0s0b0n0"]),
587 ];
588 let boot_params = vec![
589 make_boot_params(vec!["x1000c0s0b0n0"]),
590 make_boot_params(vec!["x2000c0s0b0n0"]),
591 make_boot_params(vec!["x3000c0s0b0n0"]),
592 ];
593 let result = get_restricted_boot_parameters(&groups, &boot_params);
594 assert_eq!(result.len(), 2);
595 }
596}