1use std::collections::HashMap;
25
26use manta_backend_dispatcher::{
27 error::Error, interfaces::hsm::group::GroupTrait, types::Group,
28};
29
30use super::{
31 AddHwResult, ApplyHwResult, DeleteHwResult, HwClusterMode,
32 MEMORY_CAPACITY_LCM, pin_unpin, scoring,
33};
34use crate::server::common::app_context::InfraContext;
35
36pub struct ApplyHwConfigurationParams<'a> {
50 pub mode: HwClusterMode,
52 pub target_group_name: &'a str,
54 pub parent_group_name: &'a str,
56 pub pattern: &'a str,
58 pub dryrun: bool,
61 pub create_target_group: bool,
63 pub delete_empty_parent_group: bool,
65}
66
67pub async fn apply_hw_configuration(
82 infra: &InfraContext<'_>,
83 shasta_token: &str,
84 p: ApplyHwConfigurationParams<'_>,
85) -> Result<ApplyHwResult, Error> {
86 let ApplyHwConfigurationParams {
87 mode,
88 target_group_name,
89 parent_group_name,
90 pattern,
91 dryrun,
92 create_target_group,
93 delete_empty_parent_group,
94 } = p;
95 let (user_defined_hw_component_vec, user_defined_hw_component_count_hashmap) =
96 pin_unpin::parse_hw_pattern_usize(target_group_name, pattern)?;
97
98 pin_unpin::ensure_target_group_exists(
99 infra,
100 shasta_token,
101 target_group_name,
102 dryrun,
103 create_target_group,
104 )
105 .await?;
106
107 let (
108 (
109 target_hsm_group_member_vec,
110 target_hsm_node_hw_component_count_vec,
111 target_hsm_hw_component_summary,
112 ),
113 (
114 parent_hsm_group_member_vec,
115 parent_hsm_node_hw_component_count_vec,
116 _parent_summary,
117 ),
118 ) = tokio::try_join!(
119 scoring::fetch_group_hw_inventory(
120 infra,
121 shasta_token,
122 &user_defined_hw_component_vec,
123 target_group_name,
124 MEMORY_CAPACITY_LCM,
125 ),
126 scoring::fetch_group_hw_inventory(
127 infra,
128 shasta_token,
129 &user_defined_hw_component_vec,
130 parent_group_name,
131 MEMORY_CAPACITY_LCM,
132 ),
133 )?;
134
135 tracing::info!(
136 "HSM group '{}' hw component summary: {:?}",
137 target_group_name,
138 target_hsm_hw_component_summary
139 );
140
141 pin_unpin::validate_resource_sufficiency(
142 &target_hsm_node_hw_component_count_vec,
143 &parent_hsm_node_hw_component_count_vec,
144 &user_defined_hw_component_count_hashmap,
145 )?;
146
147 let (
148 target_hsm_node_hw_component_count_vec,
149 parent_hsm_node_hw_component_count_vec,
150 ) = scoring::resolve_hw_description_to_xnames(
151 mode,
152 target_hsm_node_hw_component_count_vec,
153 parent_hsm_node_hw_component_count_vec,
154 &user_defined_hw_component_count_hashmap,
155 )?;
156
157 let target_hsm_node_vec: Vec<String> = target_hsm_node_hw_component_count_vec
158 .into_iter()
159 .map(|(xname, _)| xname)
160 .collect();
161
162 let parent_hsm_node_vec: Vec<String> = parent_hsm_node_hw_component_count_vec
163 .into_iter()
164 .map(|(xname, _)| xname)
165 .collect();
166
167 pin_unpin::apply_group_updates(
168 infra,
169 shasta_token,
170 pin_unpin::GroupUpdate {
171 target_group: target_group_name,
172 parent_group: parent_group_name,
173 old_target_members: &target_hsm_group_member_vec,
174 old_parent_members: &parent_hsm_group_member_vec,
175 new_target_members: &target_hsm_node_vec,
176 new_parent_members: &parent_hsm_node_vec,
177 dryrun,
178 delete_empty_parent: delete_empty_parent_group,
179 },
180 )
181 .await?;
182
183 Ok(ApplyHwResult {
184 target_nodes: target_hsm_node_vec,
185 parent_nodes: parent_hsm_node_vec,
186 })
187}
188
189async fn ensure_add_target_group_exists(
193 infra: &InfraContext<'_>,
194 shasta_token: &str,
195 target_hsm_group_name: &str,
196 dryrun: bool,
197 create_hsm_group: bool,
198) -> Result<(), Error> {
199 if infra
200 .backend
201 .get_group(shasta_token, target_hsm_group_name)
202 .await
203 .is_ok()
204 {
205 tracing::debug!("The group '{}' exists, good.", target_hsm_group_name);
206 return Ok(());
207 }
208 if !create_hsm_group {
209 return Err(Error::NotFound(format!(
210 "Group '{target_hsm_group_name}' does not exist, but the \
211 option to create the group was NOT \
212 specified, cannot continue."
213 )));
214 }
215 tracing::info!(
216 "Group '{}' does not exist, but the option \
217 to create the group has been selected, \
218 creating it now.",
219 target_hsm_group_name
220 );
221 if dryrun {
222 return Err(Error::BadRequest(
223 "Dryrun selected, cannot create \
224 the new group and continue."
225 .to_string(),
226 ));
227 }
228 let group = Group {
229 label: target_hsm_group_name.to_string(),
230 description: None,
231 tags: None,
232 members: None,
233 exclusive_group: Some("false".to_string()),
234 };
235 infra.backend.add_group(shasta_token, group).await?;
236 Ok(())
237}
238
239#[allow(clippy::cast_sign_loss)]
247fn compute_final_parent_summary(
248 current_summary: &HashMap<String, usize>,
249 deltas: &HashMap<String, isize>,
250 parent_group_name: &str,
251) -> Result<HashMap<String, usize>, Error> {
252 let mut final_summary: HashMap<String, usize> = HashMap::new();
253
254 for (hw_component, counter) in deltas {
255 let current = *current_summary.get(hw_component).unwrap_or(&0);
256 if *counter > current.cast_signed() {
257 return Err(Error::InsufficientResources(format!(
258 "Cannot remove more hw component '{}' \
259 ({}) than available in parent group \
260 '{}' ({})",
261 hw_component, *counter, parent_group_name, current
262 )));
263 }
264 let new_counter = current - *counter as usize;
265 final_summary.insert(hw_component.clone(), new_counter);
266 }
267
268 Ok(final_summary)
269}
270
271pub async fn add_hw_component(
296 infra: &InfraContext<'_>,
297 shasta_token: &str,
298 target_group_name: &str,
299 parent_group_name: &str,
300 pattern: &str,
301 dryrun: bool,
302 create_group: bool,
303) -> Result<AddHwResult, Error> {
304 ensure_add_target_group_exists(
305 infra,
306 shasta_token,
307 target_group_name,
308 dryrun,
309 create_group,
310 )
311 .await?;
312
313 let pattern_str = format!("{target_group_name}:{pattern}");
314 let pattern_lowercase = pattern_str.to_lowercase();
315 let mut pattern_element_vec: Vec<&str> =
316 pattern_lowercase.split(':').collect();
317 let target_name = pattern_element_vec.remove(0);
318
319 let (
320 user_defined_delta_hw_component_vec,
321 user_defined_delta_hw_component_count_hashmap,
322 ) = scoring::parse_hw_pattern(&pattern_element_vec)?;
323
324 let (
325 _parent_member_vec,
326 mut parent_hsm_node_hw_component_count_vec,
327 parent_hsm_hw_component_summary,
328 ) = scoring::fetch_group_hw_inventory(
329 infra,
330 shasta_token,
331 &user_defined_delta_hw_component_vec,
332 parent_group_name,
333 MEMORY_CAPACITY_LCM,
334 )
335 .await?;
336
337 let final_parent_hsm_hw_component_summary = compute_final_parent_summary(
338 &parent_hsm_hw_component_summary,
339 &user_defined_delta_hw_component_count_hashmap,
340 parent_group_name,
341 )?;
342
343 let scarcity_scores = scoring::calculate_hw_component_scarcity_scores(
344 &parent_hsm_node_hw_component_count_vec,
345 );
346
347 let hw_counters_to_move = pin_unpin::calculate_target_group_unpin(
348 &final_parent_hsm_hw_component_summary,
349 &final_parent_hsm_hw_component_summary
350 .keys()
351 .cloned()
352 .collect::<Vec<String>>(),
353 &mut parent_hsm_node_hw_component_count_vec,
354 &scarcity_scores,
355 )?;
356
357 let nodes_to_move: Vec<String> = hw_counters_to_move
358 .iter()
359 .map(|(xname, _)| xname.clone())
360 .collect();
361
362 let mut target_hsm_node_vec: Vec<String> = infra
363 .backend
364 .get_member_vec_from_group_name_vec(
365 shasta_token,
366 &[target_name.to_string()],
367 )
368 .await?;
369
370 target_hsm_node_vec.extend(nodes_to_move.clone());
371 target_hsm_node_vec.sort();
372
373 if !dryrun {
374 futures::future::try_join_all(nodes_to_move.iter().map(
375 |xname| async move {
376 infra
380 .backend
381 .delete_member_from_group(shasta_token, parent_group_name, xname)
382 .await?;
383 infra
384 .backend
385 .add_members_to_group(shasta_token, target_name, &[xname.as_str()])
386 .await?;
387 Ok::<(), Error>(())
388 },
389 ))
390 .await?;
391 }
392
393 let parent_nodes: Vec<String> = parent_hsm_node_hw_component_count_vec
394 .iter()
395 .map(|(xname, _)| xname.clone())
396 .collect();
397
398 Ok(AddHwResult {
399 nodes_moved: nodes_to_move,
400 target_nodes: target_hsm_node_vec,
401 parent_nodes,
402 })
403}
404
405async fn handle_empty_target(
409 infra: &InfraContext<'_>,
410 shasta_token: &str,
411 target_hsm_group_name: &str,
412 dryrun: bool,
413 delete_hsm_group: bool,
414) -> Result<(), Error> {
415 tracing::info!(
416 "The target HSM group {} is already empty, cannot \
417 remove hardware from it.",
418 target_hsm_group_name
419 );
420
421 if dryrun || !delete_hsm_group {
422 tracing::info!(
423 "The option to delete empty groups has NOT been \
424 selected, or the dryrun has been enabled. We \
425 are done with this action."
426 );
427 return Ok(());
428 }
429
430 tracing::info!(
431 "The option to delete empty groups has been \
432 selected, removing it."
433 );
434 match infra
435 .backend
436 .delete_group(shasta_token, target_hsm_group_name)
437 .await
438 {
439 Ok(_) => {
440 tracing::info!(
441 "HSM group removed successfully, we are \
442 done with this action."
443 );
444 }
445 Err(e) => tracing::debug!(
446 "Error removing the HSM group. This always \
447 fails, ignore please. Reported: {}",
448 e
449 ),
450 }
451 Ok(())
452}
453
454#[allow(clippy::cast_sign_loss)]
459fn compute_delete_final_summary(
460 current_summary: &HashMap<String, usize>,
461 deltas: &HashMap<String, isize>,
462) -> Result<HashMap<String, usize>, Error> {
463 let mut final_summary: HashMap<String, usize> = HashMap::new();
464
465 for (hw_component, counter) in deltas {
466 let current = *current_summary.get(hw_component).ok_or_else(|| {
467 Error::NotFound(format!(
468 "hw component '{hw_component}' not found in target HSM \
469 hw component summary"
470 ))
471 })?;
472
473 final_summary.insert(hw_component.clone(), current - *counter as usize);
474 }
475
476 Ok(final_summary)
477}
478
479async fn apply_node_moves(
481 infra: &InfraContext<'_>,
482 shasta_token: &str,
483 target_group: &str,
484 parent_group: &str,
485 nodes: &[String],
486 target_will_be_empty: bool,
487 delete_hsm_group: bool,
488) -> Result<(), Error> {
489 futures::future::try_join_all(nodes.iter().map(|xname| async move {
490 infra
494 .backend
495 .delete_member_from_group(shasta_token, target_group, xname.as_str())
496 .await?;
497 infra
498 .backend
499 .add_members_to_group(shasta_token, parent_group, &[xname.as_str()])
500 .await?;
501 Ok::<(), Error>(())
502 }))
503 .await?;
504
505 if target_will_be_empty {
506 if delete_hsm_group {
507 tracing::info!(
508 "HSM group {} is now empty and the option to \
509 delete empty groups has been selected, \
510 removing it.",
511 target_group
512 );
513 match infra.backend.delete_group(shasta_token, target_group).await {
514 Ok(_) => tracing::info!("HSM group removed successfully."),
515 Err(e) => tracing::debug!(
516 "Error removing the HSM group. This always \
517 fails, ignore please. Reported: {}",
518 e
519 ),
520 }
521 } else {
522 tracing::debug!(
523 "HSM group {} is now empty and the option to \
524 delete empty groups has NOT been selected, \
525 will not remove it.",
526 target_group
527 );
528 }
529 }
530
531 Ok(())
532}
533
534pub async fn delete_hw_component(
555 infra: &InfraContext<'_>,
556 token: &str,
557 target_group_name: &str,
558 parent_group_name: &str,
559 pattern: &str,
560 dryrun: bool,
561 delete_group: bool,
562) -> Result<DeleteHwResult, Error> {
563 match infra.backend.get_group(token, target_group_name).await {
564 Ok(_) => {}
565 Err(_) => {
566 return Err(Error::NotFound(format!(
567 "HSM group {target_group_name} does not exist, cannot remove hw from it."
568 )));
569 }
570 }
571
572 let pattern_str = format!("{target_group_name}:{pattern}");
573 let pattern_lowercase = pattern_str.to_lowercase();
574 let mut pattern_element_vec: Vec<&str> =
575 pattern_lowercase.split(':').collect();
576 let target_name = pattern_element_vec.remove(0);
577
578 let (
579 user_defined_delta_hw_component_vec,
580 user_defined_delta_hw_component_count_hashmap,
581 ) = scoring::parse_hw_pattern(&pattern_element_vec)?;
582
583 let (
584 (
585 target_hsm_group_member_vec,
586 mut target_hsm_node_hw_component_count_vec,
587 target_hsm_hw_component_summary,
588 ),
589 (
590 parent_hsm_group_member_vec,
591 parent_hsm_node_hw_component_count_vec,
592 _parent_summary,
593 ),
594 ) = tokio::try_join!(
595 scoring::fetch_group_hw_inventory(
596 infra,
597 token,
598 &user_defined_delta_hw_component_vec,
599 target_name,
600 MEMORY_CAPACITY_LCM,
601 ),
602 scoring::fetch_group_hw_inventory(
603 infra,
604 token,
605 &user_defined_delta_hw_component_vec,
606 parent_group_name,
607 MEMORY_CAPACITY_LCM,
608 ),
609 )?;
610
611 if target_hsm_node_hw_component_count_vec.is_empty() {
612 handle_empty_target(infra, token, target_name, dryrun, delete_group)
613 .await?;
614 return Ok(DeleteHwResult {
615 nodes_moved: vec![],
616 target_nodes: vec![],
617 parent_nodes: vec![],
618 });
619 }
620
621 let combined = [
622 target_hsm_node_hw_component_count_vec.clone(),
623 parent_hsm_node_hw_component_count_vec.clone(),
624 ]
625 .concat();
626 let scarcity_scores =
627 scoring::calculate_hw_component_scarcity_scores(&combined);
628
629 let final_target_summary = compute_delete_final_summary(
630 &target_hsm_hw_component_summary,
631 &user_defined_delta_hw_component_count_hashmap,
632 )?;
633
634 let hw_counters_to_move = pin_unpin::calculate_target_group_unpin(
635 &final_target_summary,
636 &final_target_summary
637 .keys()
638 .cloned()
639 .collect::<Vec<String>>(),
640 &mut target_hsm_node_hw_component_count_vec,
641 &scarcity_scores,
642 )?;
643
644 let nodes_to_move: Vec<String> = hw_counters_to_move
645 .iter()
646 .map(|(xname, _)| xname.clone())
647 .collect();
648
649 let mut parent_nodes: Vec<String> = parent_hsm_group_member_vec;
650 parent_nodes.extend(nodes_to_move.clone());
651 parent_nodes.sort();
652
653 let target_nodes: Vec<String> = target_hsm_node_hw_component_count_vec
654 .iter()
655 .map(|(xname, _)| xname.clone())
656 .collect();
657
658 if !dryrun {
659 apply_node_moves(
660 infra,
661 token,
662 target_name,
663 parent_group_name,
664 &nodes_to_move,
665 target_hsm_group_member_vec.len() == nodes_to_move.len(),
666 delete_group,
667 )
668 .await?;
669 }
670
671 Ok(DeleteHwResult {
672 nodes_moved: nodes_to_move,
673 target_nodes,
674 parent_nodes,
675 })
676}