manta_server/service/
node.rs

1//! HSM node queries, registration, and deletion, with rollback on partial failure.
2
3use csm_rs::node::types::NodeDetails;
4use manta_backend_dispatcher::error::Error;
5use manta_backend_dispatcher::interfaces::hsm::component::ComponentTrait;
6use manta_backend_dispatcher::interfaces::hsm::group::GroupTrait;
7use manta_backend_dispatcher::interfaces::hsm::hardware_inventory::HardwareInventory;
8use manta_backend_dispatcher::types::{
9  ComponentArrayPostArray, ComponentCreate, HWInventoryByLocationList,
10};
11use std::{fs::File, io::BufReader, path::PathBuf};
12
13use crate::server::common;
14use crate::server::common::app_context::InfraContext;
15pub use manta_shared::shared::params::node::GetNodesParams;
16
17/// Fetch node details for the given xname expression.
18pub async fn get_nodes(
19  infra: &InfraContext<'_>,
20  token: &str,
21  params: &GetNodesParams,
22) -> Result<Vec<NodeDetails>, Error> {
23  let node_list = common::node_ops::resolve_hosts_expression(
24    infra.backend,
25    token,
26    &params.xname,
27    params.include_siblings,
28  )
29  .await?;
30
31  if node_list.is_empty() {
32    return Err(Error::BadRequest(
33      "The list of nodes to operate is empty. Nothing to do".to_string(),
34    ));
35  }
36
37  let mut node_details_list = csm_rs::node::utils::get_node_details(
38    token,
39    infra.shasta_base_url,
40    infra.shasta_root_cert,
41    infra.socks5_proxy,
42    node_list.to_vec(),
43  )
44  .await
45  .map_err(|e: csm_rs::error::Error| -> Error { e.into() })?;
46
47  // Apply status filter
48  if let Some(ref status) = params.status_filter {
49    node_details_list.retain(|nd| {
50      nd.power_status.eq_ignore_ascii_case(status)
51        || nd.configuration_status.eq_ignore_ascii_case(status)
52    });
53  }
54
55  node_details_list.sort_by(|a, b| a.xname.cmp(&b.xname));
56
57  Ok(node_details_list)
58}
59
60// `compute_summary_status` moved to `manta_shared::shared::cluster_status` —
61// only CLI display code calls it.
62
63/// Delete a node by its xname/ID.
64pub async fn delete_node(
65  infra: &InfraContext<'_>,
66  token: &str,
67  id: &str,
68) -> Result<(), Error> {
69  infra.backend.delete_node(token, id).await.map(|_| ())
70}
71
72/// Register a new node, optionally add hardware inventory,
73/// and assign it to an HSM group.
74///
75/// Rolls back (deletes the node) if any step after creation fails.
76pub async fn add_node(
77  infra: &InfraContext<'_>,
78  token: &str,
79  id: &str,
80  group: &str,
81  enabled: bool,
82  arch_opt: Option<String>,
83  hardware_file_path: Option<&PathBuf>,
84) -> Result<(), Error> {
85  let backend = infra.backend;
86
87  // Create node
88  let component = ComponentCreate {
89    id: id.to_string(),
90    state: "Unknown".to_string(),
91    flag: None,
92    enabled: Some(enabled),
93    software_status: None,
94    role: None,
95    sub_role: None,
96    nid: None,
97    subtype: None,
98    net_type: None,
99    arch: arch_opt,
100    class: None,
101  };
102
103  let components = ComponentArrayPostArray {
104    components: vec![component],
105    force: Some(true),
106  };
107
108  backend.post_nodes(token, components).await?;
109
110  tracing::info!("Node saved '{}'", id);
111
112  // Parse and add hardware inventory if provided
113  let hw_inventory_opt: Option<HWInventoryByLocationList> =
114    if let Some(hardware_file) = hardware_file_path {
115      let file = match File::open(hardware_file) {
116        Ok(f) => f,
117        Err(e) => {
118          rollback_node(backend, token, id).await;
119          return Err(e.into());
120        }
121      };
122      let reader = BufReader::new(file);
123      let hw_inventory_value: serde_json::Value =
124        match serde_json::from_reader(reader) {
125          Ok(v) => v,
126          Err(e) => {
127            rollback_node(backend, token, id).await;
128            return Err(e.into());
129          }
130        };
131      Some(
132        match serde_json::from_value::<HWInventoryByLocationList>(
133          hw_inventory_value,
134        ) {
135          Ok(v) => v,
136          Err(e) => {
137            rollback_node(backend, token, id).await;
138            return Err(e.into());
139          }
140        },
141      )
142    } else {
143      None
144    };
145
146  if let Some(hw_inventory) = hw_inventory_opt {
147    tracing::info!("Adding hardware inventory for '{}'", id);
148    if let Err(error) =
149      backend.post_inventory_hardware(token, hw_inventory).await
150    {
151      rollback_node(backend, token, id).await;
152      return Err(error);
153    }
154  }
155
156  // Add node to group
157  if let Err(error) = backend.post_member(token, group, id).await {
158    rollback_node(backend, token, id).await;
159    return Err(error);
160  }
161
162  Ok(())
163}
164
165/// Rollback helper: attempt to delete a node that was partially created.
166async fn rollback_node(
167  backend: &crate::manta_backend_dispatcher::StaticBackendDispatcher,
168  token: &str,
169  id: &str,
170) {
171  tracing::warn!("Rolling back: attempting to delete node '{}'", id);
172  let delete_node_rslt = backend.delete_node(token, id).await;
173  if delete_node_rslt.is_ok() {
174    tracing::info!("Rollback: node '{}' deleted", id);
175  }
176}