manta_server/service/hw_cluster/
apply.rs

1//! High-level coordinators: `apply_hw_configuration` (pin/unpin),
2//! `add_hw_component`, `delete_hw_component`. These are the functions
3//! the server handlers call directly.
4
5use std::collections::HashMap;
6
7use manta_backend_dispatcher::{
8  error::Error, interfaces::hsm::group::GroupTrait, types::Group,
9};
10
11use super::{
12  AddHwResult, ApplyHwResult, DeleteHwResult, HwClusterMode,
13  MEMORY_CAPACITY_LCM, pin_unpin, scoring,
14};
15use crate::manta_backend_dispatcher::StaticBackendDispatcher;
16
17/// Core logic for hardware cluster pin/unpin — no terminal interaction.
18#[allow(clippy::too_many_arguments)]
19pub async fn apply_hw_configuration(
20  backend: &StaticBackendDispatcher,
21  mode: HwClusterMode,
22  shasta_token: &str,
23  target_hsm_group_name: &str,
24  parent_hsm_group_name: &str,
25  pattern: &str,
26  dryrun: bool,
27  create_target_hsm_group: bool,
28  delete_empty_parent_hsm_group: bool,
29) -> Result<ApplyHwResult, Error> {
30  let (user_defined_hw_component_vec, user_defined_hw_component_count_hashmap) =
31    pin_unpin::parse_hw_pattern_usize(target_hsm_group_name, pattern)?;
32
33  pin_unpin::ensure_target_group_exists(
34    backend,
35    shasta_token,
36    target_hsm_group_name,
37    dryrun,
38    create_target_hsm_group,
39  )
40  .await?;
41
42  let (
43    target_hsm_group_member_vec,
44    target_hsm_node_hw_component_count_vec,
45    target_hsm_hw_component_summary,
46  ) = scoring::fetch_hsm_hw_inventory(
47    backend,
48    shasta_token,
49    &user_defined_hw_component_vec,
50    target_hsm_group_name,
51    MEMORY_CAPACITY_LCM,
52  )
53  .await?;
54
55  tracing::info!(
56    "HSM group '{}' hw component summary: {:?}",
57    target_hsm_group_name,
58    target_hsm_hw_component_summary
59  );
60
61  let (
62    parent_hsm_group_member_vec,
63    parent_hsm_node_hw_component_count_vec,
64    _parent_summary,
65  ) = scoring::fetch_hsm_hw_inventory(
66    backend,
67    shasta_token,
68    &user_defined_hw_component_vec,
69    parent_hsm_group_name,
70    MEMORY_CAPACITY_LCM,
71  )
72  .await?;
73
74  pin_unpin::validate_resource_sufficiency(
75    &target_hsm_node_hw_component_count_vec,
76    &parent_hsm_node_hw_component_count_vec,
77    &user_defined_hw_component_count_hashmap,
78  )?;
79
80  let (
81    target_hsm_node_hw_component_count_vec,
82    parent_hsm_node_hw_component_count_vec,
83  ) = scoring::resolve_hw_description_to_xnames(
84    mode,
85    target_hsm_node_hw_component_count_vec,
86    parent_hsm_node_hw_component_count_vec,
87    user_defined_hw_component_count_hashmap,
88  )
89  .await?;
90
91  let target_hsm_node_vec: Vec<String> = target_hsm_node_hw_component_count_vec
92    .into_iter()
93    .map(|(xname, _)| xname)
94    .collect();
95
96  let parent_hsm_node_vec: Vec<String> = parent_hsm_node_hw_component_count_vec
97    .into_iter()
98    .map(|(xname, _)| xname)
99    .collect();
100
101  pin_unpin::apply_group_updates(
102    backend,
103    shasta_token,
104    target_hsm_group_name,
105    parent_hsm_group_name,
106    &target_hsm_group_member_vec,
107    &parent_hsm_group_member_vec,
108    &target_hsm_node_vec,
109    &parent_hsm_node_vec,
110    dryrun,
111    delete_empty_parent_hsm_group,
112  )
113  .await?;
114
115  Ok(ApplyHwResult {
116    target_nodes: target_hsm_node_vec,
117    parent_nodes: parent_hsm_node_vec,
118  })
119}
120
121// ── add_hw_component ─────────────────────────────────────────────────────────
122
123/// Ensure the target HSM group exists for add-hw-component, creating it if needed.
124async fn ensure_add_target_group_exists(
125  backend: &StaticBackendDispatcher,
126  shasta_token: &str,
127  target_hsm_group_name: &str,
128  dryrun: bool,
129  create_hsm_group: bool,
130) -> Result<(), Error> {
131  match backend.get_group(shasta_token, target_hsm_group_name).await {
132    Ok(_) => {
133      tracing::debug!("The group '{}' exists, good.", target_hsm_group_name);
134      Ok(())
135    }
136    Err(_) => {
137      if !create_hsm_group {
138        return Err(Error::NotFound(format!(
139          "Group '{target_hsm_group_name}' does not exist, but the \
140           option to create the group was NOT \
141           specified, cannot continue."
142        )));
143      }
144      tracing::info!(
145        "Group '{}' does not exist, but the option \
146         to create the group has been selected, \
147         creating it now.",
148        target_hsm_group_name
149      );
150      if dryrun {
151        return Err(Error::BadRequest(
152          "Dryrun selected, cannot create \
153           the new group and continue."
154            .to_string(),
155        ));
156      }
157      let group = Group {
158        label: target_hsm_group_name.to_string(),
159        description: None,
160        tags: None,
161        members: None,
162        exclusive_group: Some("false".to_string()),
163      };
164      backend.add_group(shasta_token, group).await?;
165      Ok(())
166    }
167  }
168}
169
170/// Compute the final parent HSM hw component summary after subtracting user-requested deltas.
171fn compute_final_parent_summary(
172  current_summary: &HashMap<String, usize>,
173  deltas: &HashMap<String, isize>,
174  parent_group_name: &str,
175) -> Result<HashMap<String, usize>, Error> {
176  let mut final_summary: HashMap<String, usize> = HashMap::new();
177
178  for (hw_component, counter) in deltas {
179    let current = *current_summary.get(hw_component).unwrap_or(&0);
180    if *counter > current as isize {
181      return Err(Error::InsufficientResources(format!(
182        "Cannot remove more hw component '{}' \
183         ({}) than available in parent group \
184         '{}' ({})",
185        hw_component, *counter, parent_group_name, current
186      )));
187    }
188    let new_counter = current - *counter as usize;
189    final_summary.insert(hw_component.to_string(), new_counter);
190  }
191
192  Ok(final_summary)
193}
194
195/// Core logic for adding hardware components to a cluster group.
196/// No terminal interaction — suitable for both CLI and HTTP callers.
197pub async fn add_hw_component(
198  backend: &StaticBackendDispatcher,
199  shasta_token: &str,
200  target_hsm_group_name: &str,
201  parent_hsm_group_name: &str,
202  pattern: &str,
203  dryrun: bool,
204  create_hsm_group: bool,
205) -> Result<AddHwResult, Error> {
206  ensure_add_target_group_exists(
207    backend,
208    shasta_token,
209    target_hsm_group_name,
210    dryrun,
211    create_hsm_group,
212  )
213  .await?;
214
215  let pattern_str = format!("{target_hsm_group_name}:{pattern}");
216  let pattern_lowercase = pattern_str.to_lowercase();
217  let mut pattern_element_vec: Vec<&str> =
218    pattern_lowercase.split(':').collect();
219  let target_name = pattern_element_vec.remove(0);
220
221  let (
222    user_defined_delta_hw_component_vec,
223    user_defined_delta_hw_component_count_hashmap,
224  ) = scoring::parse_hw_pattern(&pattern_element_vec)?;
225
226  let (
227    _parent_member_vec,
228    mut parent_hsm_node_hw_component_count_vec,
229    parent_hsm_hw_component_summary,
230  ) = scoring::fetch_hsm_hw_inventory(
231    backend,
232    shasta_token,
233    &user_defined_delta_hw_component_vec,
234    parent_hsm_group_name,
235    MEMORY_CAPACITY_LCM,
236  )
237  .await?;
238
239  let final_parent_hsm_hw_component_summary = compute_final_parent_summary(
240    &parent_hsm_hw_component_summary,
241    &user_defined_delta_hw_component_count_hashmap,
242    parent_hsm_group_name,
243  )?;
244
245  let scarcity_scores = scoring::calculate_hw_component_scarcity_scores(
246    &parent_hsm_node_hw_component_count_vec,
247  )
248  .await;
249
250  let hw_counters_to_move = pin_unpin::calculate_target_hsm_unpin(
251    &final_parent_hsm_hw_component_summary,
252    &final_parent_hsm_hw_component_summary
253      .keys()
254      .cloned()
255      .collect::<Vec<String>>(),
256    &mut parent_hsm_node_hw_component_count_vec,
257    &scarcity_scores,
258  )?;
259
260  let nodes_to_move: Vec<String> = hw_counters_to_move
261    .iter()
262    .map(|(xname, _)| xname.clone())
263    .collect();
264
265  let mut target_hsm_node_vec: Vec<String> = backend
266    .get_member_vec_from_group_name_vec(
267      shasta_token,
268      &[target_name.to_string()],
269    )
270    .await?;
271
272  target_hsm_node_vec.extend(nodes_to_move.clone());
273  target_hsm_node_vec.sort();
274
275  if !dryrun {
276    for xname in &nodes_to_move {
277      backend
278        .delete_member_from_group(shasta_token, parent_hsm_group_name, xname)
279        .await?;
280
281      let _ = backend
282        .add_members_to_group(shasta_token, target_name, &[xname.as_str()])
283        .await?;
284    }
285  }
286
287  let parent_nodes: Vec<String> = parent_hsm_node_hw_component_count_vec
288    .iter()
289    .map(|(xname, _)| xname.clone())
290    .collect();
291
292  Ok(AddHwResult {
293    nodes_moved: nodes_to_move,
294    target_nodes: target_hsm_node_vec,
295    parent_nodes,
296  })
297}
298
299// ── delete_hw_component ──────────────────────────────────────────────────────
300
301/// Handle the case when target HSM group is already empty.
302async fn handle_empty_target(
303  backend: &StaticBackendDispatcher,
304  shasta_token: &str,
305  target_hsm_group_name: &str,
306  dryrun: bool,
307  delete_hsm_group: bool,
308) -> Result<(), Error> {
309  tracing::info!(
310    "The target HSM group {} is already empty, cannot \
311     remove hardware from it.",
312    target_hsm_group_name
313  );
314
315  if dryrun || !delete_hsm_group {
316    tracing::info!(
317      "The option to delete empty groups has NOT been \
318       selected, or the dryrun has been enabled. We \
319       are done with this action."
320    );
321    return Ok(());
322  }
323
324  tracing::info!(
325    "The option to delete empty groups has been \
326     selected, removing it."
327  );
328  match backend
329    .delete_group(shasta_token, target_hsm_group_name)
330    .await
331  {
332    Ok(_) => {
333      tracing::info!(
334        "HSM group removed successfully, we are \
335         done with this action."
336      );
337    }
338    Err(e) => tracing::debug!(
339      "Error removing the HSM group. This always \
340       fails, ignore please. Reported: {}",
341      e
342    ),
343  }
344  Ok(())
345}
346
347/// Compute the final target HSM hw component summary after subtracting deltas.
348fn compute_delete_final_summary(
349  current_summary: &HashMap<String, usize>,
350  deltas: &HashMap<String, isize>,
351) -> Result<HashMap<String, usize>, Error> {
352  let mut final_summary: HashMap<String, usize> = HashMap::new();
353
354  for (hw_component, counter) in deltas {
355    let current = *current_summary.get(hw_component).ok_or_else(|| {
356      Error::NotFound(format!(
357        "hw component '{hw_component}' not found in target HSM \
358           hw component summary"
359      ))
360    })?;
361
362    final_summary.insert(hw_component.to_string(), current - *counter as usize);
363  }
364
365  Ok(final_summary)
366}
367
368/// Move nodes between HSM groups: delete from target, add to parent.
369async fn apply_node_moves(
370  backend: &StaticBackendDispatcher,
371  shasta_token: &str,
372  target_group: &str,
373  parent_group: &str,
374  nodes: &[String],
375  target_will_be_empty: bool,
376  delete_hsm_group: bool,
377) -> Result<(), Error> {
378  for xname in nodes {
379    backend
380      .delete_member_from_group(shasta_token, target_group, xname.as_str())
381      .await?;
382
383    backend
384      .add_members_to_group(shasta_token, parent_group, &[xname.as_str()])
385      .await?;
386  }
387
388  if target_will_be_empty {
389    if delete_hsm_group {
390      tracing::info!(
391        "HSM group {} is now empty and the option to \
392         delete empty groups has been selected, \
393         removing it.",
394        target_group
395      );
396      match backend.delete_group(shasta_token, target_group).await {
397        Ok(_) => tracing::info!("HSM group removed successfully."),
398        Err(e) => tracing::debug!(
399          "Error removing the HSM group. This always \
400           fails, ignore please. Reported: {}",
401          e
402        ),
403      }
404    } else {
405      tracing::debug!(
406        "HSM group {} is now empty and the option to \
407         delete empty groups has NOT been selected, \
408         will not remove it.",
409        target_group
410      )
411    }
412  }
413
414  Ok(())
415}
416
417/// Core logic for removing hardware components from a cluster group.
418/// No terminal interaction — suitable for both CLI and HTTP callers.
419pub async fn delete_hw_component(
420  backend: &StaticBackendDispatcher,
421  token: &str,
422  target_hsm_group_name: &str,
423  parent_hsm_group_name: &str,
424  pattern: &str,
425  dryrun: bool,
426  delete_hsm_group: bool,
427) -> Result<DeleteHwResult, Error> {
428  match backend.get_group(token, target_hsm_group_name).await {
429    Ok(_) => {}
430    Err(_) => {
431      return Err(Error::NotFound(format!(
432        "HSM group {target_hsm_group_name} does not exist, cannot remove hw from it."
433      )));
434    }
435  }
436
437  let pattern_str = format!("{target_hsm_group_name}:{pattern}");
438  let pattern_lowercase = pattern_str.to_lowercase();
439  let mut pattern_element_vec: Vec<&str> =
440    pattern_lowercase.split(':').collect();
441  let target_name = pattern_element_vec.remove(0);
442
443  let (
444    user_defined_delta_hw_component_vec,
445    user_defined_delta_hw_component_count_hashmap,
446  ) = scoring::parse_hw_pattern(&pattern_element_vec)?;
447
448  let (
449    target_hsm_group_member_vec,
450    mut target_hsm_node_hw_component_count_vec,
451    target_hsm_hw_component_summary,
452  ) = scoring::fetch_hsm_hw_inventory(
453    backend,
454    token,
455    &user_defined_delta_hw_component_vec,
456    target_name,
457    MEMORY_CAPACITY_LCM,
458  )
459  .await?;
460
461  if target_hsm_node_hw_component_count_vec.is_empty() {
462    handle_empty_target(backend, token, target_name, dryrun, delete_hsm_group)
463      .await?;
464    return Ok(DeleteHwResult {
465      nodes_moved: vec![],
466      target_nodes: vec![],
467      parent_nodes: vec![],
468    });
469  }
470
471  let (
472    parent_hsm_group_member_vec,
473    parent_hsm_node_hw_component_count_vec,
474    _parent_summary,
475  ) = scoring::fetch_hsm_hw_inventory(
476    backend,
477    token,
478    &user_defined_delta_hw_component_vec,
479    parent_hsm_group_name,
480    MEMORY_CAPACITY_LCM,
481  )
482  .await?;
483
484  let combined = [
485    target_hsm_node_hw_component_count_vec.clone(),
486    parent_hsm_node_hw_component_count_vec.clone(),
487  ]
488  .concat();
489  let scarcity_scores =
490    scoring::calculate_hw_component_scarcity_scores(&combined).await;
491
492  let final_target_summary = compute_delete_final_summary(
493    &target_hsm_hw_component_summary,
494    &user_defined_delta_hw_component_count_hashmap,
495  )?;
496
497  let hw_counters_to_move = pin_unpin::calculate_target_hsm_unpin(
498    &final_target_summary,
499    &final_target_summary
500      .keys()
501      .cloned()
502      .collect::<Vec<String>>(),
503    &mut target_hsm_node_hw_component_count_vec,
504    &scarcity_scores,
505  )?;
506
507  let nodes_to_move: Vec<String> = hw_counters_to_move
508    .iter()
509    .map(|(xname, _)| xname.clone())
510    .collect();
511
512  let mut parent_nodes: Vec<String> = parent_hsm_group_member_vec;
513  parent_nodes.extend(nodes_to_move.clone());
514  parent_nodes.sort();
515
516  let target_nodes: Vec<String> = target_hsm_node_hw_component_count_vec
517    .iter()
518    .map(|(xname, _)| xname.clone())
519    .collect();
520
521  if !dryrun {
522    apply_node_moves(
523      backend,
524      token,
525      target_name,
526      parent_hsm_group_name,
527      &nodes_to_move,
528      target_hsm_group_member_vec.len() == nodes_to_move.len(),
529      delete_hsm_group,
530    )
531    .await?;
532  }
533
534  Ok(DeleteHwResult {
535    nodes_moved: nodes_to_move,
536    target_nodes,
537    parent_nodes,
538  })
539}