1use std::sync::Arc;
16use std::time::Instant;
17
18use manta_backend_dispatcher::error::Error;
19use manta_backend_dispatcher::interfaces::hsm::group::GroupTrait;
20use manta_backend_dispatcher::interfaces::hsm::{
21 component::ComponentTrait, hardware_inventory::HardwareInventory,
22};
23use manta_backend_dispatcher::types::NodeSummary;
24use tokio::sync::Semaphore;
25
26use crate::server::common::app_context::InfraContext;
27use crate::service::authorization::validate_user_group_members_access;
28use crate::service::node_ops::from_hosts_expression_to_xname_vec;
29pub use manta_shared::types::api::hardware::{
30 GetHardwareClusterParams, GetHardwareNodesListParams,
31};
32
33const HW_INVENTORY_CONCURRENCY_LIMIT: usize = 15;
35
36pub struct HardwareClusterResult {
40 pub hsm_group_name: String,
44 pub node_summaries: Vec<NodeSummary>,
46}
47
48async fn fetch_node_summaries(
51 infra: &InfraContext<'_>,
52 token: &str,
53 xnames: &[String],
54) -> Vec<NodeSummary> {
55 let mut tasks = tokio::task::JoinSet::new();
56 let sem = Arc::new(Semaphore::new(HW_INVENTORY_CONCURRENCY_LIMIT));
57
58 let n = xnames.len();
59 let width = n.checked_ilog10().unwrap_or(0) as usize + 1;
60
61 for (i, xname) in xnames.iter().enumerate() {
62 tracing::info!(
63 "\rGetting hw components for node '{xname}' [{:>width$}/{n}]",
64 i + 1
65 );
66
67 let backend_cp = infra.backend_clone();
68 let token_str = token.to_string();
69 let xname_str = xname.clone();
70 let permit = Arc::clone(&sem).acquire_owned().await;
71
72 tasks.spawn(async move {
73 let _permit = permit;
74 let hw_inventory_typed = backend_cp
75 .get_inventory_hardware_query(
76 &token_str, &xname_str, None, None, None, None, None,
77 )
78 .await;
79
80 let node_hw_opt = match hw_inventory_typed {
85 Ok(hw_inv) => serde_json::to_value(&hw_inv)
86 .ok()
87 .and_then(|v| v.pointer("/Nodes/0").cloned()),
88 Err(e) => {
89 tracing::error!(
90 "Failed to get HW inventory for '{}': {}",
91 xname_str,
92 e
93 );
94 None
95 }
96 };
97
98 match node_hw_opt {
99 Some(v) => NodeSummary::from_csm_value(v),
100 None => NodeSummary {
101 xname: xname_str,
102 ..Default::default()
103 },
104 }
105 });
106 }
107
108 let mut summaries = Vec::with_capacity(n);
109 while let Some(res) = tasks.join_next().await {
110 match res {
111 Ok(s) => summaries.push(s),
112 Err(e) => {
113 tracing::error!("Failed fetching node hardware information: {}", e);
114 }
115 }
116 }
117 summaries
118}
119
120pub async fn get_hardware_cluster(
139 infra: &InfraContext<'_>,
140 token: &str,
141 params: &GetHardwareClusterParams,
142) -> Result<HardwareClusterResult, Error> {
143 let (_, target_group_vec) =
151 crate::service::group::resolve_target_and_available_groups(
152 infra,
153 token,
154 params.group_name.as_deref(),
155 )
156 .await?;
157
158 let hsm_group_name = target_group_vec
159 .first()
160 .ok_or_else(|| {
161 Error::NotFound("No HSM groups available for this user".to_string())
162 })?
163 .clone();
164
165 let hsm_group = infra.backend.get_group(token, &hsm_group_name).await?;
166
167 let members = hsm_group
168 .members
169 .unwrap_or_default()
170 .ids
171 .unwrap_or_default();
172
173 if members.is_empty() {
174 tracing::warn!("HSM group '{}' has no members", hsm_group.label);
175 }
176
177 tracing::debug!(
178 "Get HW artifacts for nodes in HSM group '{}' and members {:?}",
179 hsm_group.label,
180 members
181 );
182
183 let start_total = Instant::now();
184 let node_summaries = fetch_node_summaries(infra, token, &members).await;
185 tracing::info!(
186 "Time elapsed getting hw inventory for HSM '{}': {:?}",
187 hsm_group_name,
188 start_total.elapsed()
189 );
190
191 Ok(HardwareClusterResult {
192 hsm_group_name,
193 node_summaries,
194 })
195}
196
197pub struct HardwareNodesListResult {
201 pub node_summaries: Vec<NodeSummary>,
203}
204
205pub async fn get_hardware_nodes_list(
226 infra: &InfraContext<'_>,
227 token: &str,
228 params: &GetHardwareNodesListParams,
229) -> Result<HardwareNodesListResult, Error> {
230 let node_metadata_available_vec =
231 infra.backend.get_node_metadata_available(token).await?;
232
233 let node_list = from_hosts_expression_to_xname_vec(
234 ¶ms.host_expression,
235 false,
236 &node_metadata_available_vec,
237 )?;
238
239 if node_list.is_empty() {
240 return Err(Error::BadRequest(
241 "The list of nodes to operate is empty. Nothing to do".to_string(),
242 ));
243 }
244
245 validate_user_group_members_access(infra, token, &node_list).await?;
247
248 let node_summaries = fetch_node_summaries(infra, token, &node_list).await;
249 Ok(HardwareNodesListResult { node_summaries })
250}
251
252#[cfg(test)]
257use manta_shared::types::cluster_status::calculate_group_hw_component_summary;
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use manta_backend_dispatcher::types::{
263 ArtifactSummary, ArtifactType, NodeSummary,
264 };
265
266 fn make_artifact(
268 art_type: ArtifactType,
269 info: Option<&str>,
270 ) -> ArtifactSummary {
271 ArtifactSummary {
272 xname: "x0".to_string(),
273 r#type: art_type,
274 info: info.map(String::from),
275 }
276 }
277
278 #[test]
279 fn summary_counts_processors_and_accels() {
280 let nodes = vec![NodeSummary {
281 xname: "x1000c0s0b0n0".to_string(),
282 processors: vec![
283 make_artifact(ArtifactType::Processor, Some("AMD EPYC 7742")),
284 make_artifact(ArtifactType::Processor, Some("AMD EPYC 7742")),
285 ],
286 node_accels: vec![make_artifact(
287 ArtifactType::NodeAccel,
288 Some("NVIDIA A100"),
289 )],
290 memory: vec![],
291 node_hsn_nics: vec![],
292 ..Default::default()
293 }];
294 let summary = calculate_group_hw_component_summary(&nodes);
295 assert_eq!(summary.get("AMD EPYC 7742"), Some(&2));
296 assert_eq!(summary.get("NVIDIA A100"), Some(&1));
297 }
298
299 #[test]
300 fn summary_converts_memory_mib_to_gib() {
301 let nodes = vec![NodeSummary {
302 xname: "x1000c0s0b0n0".to_string(),
303 processors: vec![],
304 node_accels: vec![],
305 memory: vec![
306 ArtifactSummary {
307 xname: "x0".to_string(),
308 r#type: ArtifactType::Memory,
309 info: Some("16384 MiB".to_string()),
310 },
311 ArtifactSummary {
312 xname: "x0".to_string(),
313 r#type: ArtifactType::Memory,
314 info: Some("16384 MiB".to_string()),
315 },
316 ],
317 node_hsn_nics: vec![],
318 ..Default::default()
319 }];
320 let summary = calculate_group_hw_component_summary(&nodes);
321 assert_eq!(summary.get("Memory (GiB)"), Some(&32));
322 }
323
324 #[test]
325 fn summary_aggregates_across_multiple_nodes() {
326 let nodes = vec![
327 NodeSummary {
328 xname: "n1".to_string(),
329 processors: vec![make_artifact(
330 ArtifactType::Processor,
331 Some("AMD EPYC 7742"),
332 )],
333 ..Default::default()
334 },
335 NodeSummary {
336 xname: "n2".to_string(),
337 processors: vec![
338 make_artifact(ArtifactType::Processor, Some("AMD EPYC 7742")),
339 make_artifact(ArtifactType::Processor, Some("Intel Xeon Gold")),
340 ],
341 ..Default::default()
342 },
343 ];
344 let summary = calculate_group_hw_component_summary(&nodes);
345 assert_eq!(summary.get("AMD EPYC 7742"), Some(&2));
346 assert_eq!(summary.get("Intel Xeon Gold"), Some(&1));
347 }
348
349 #[test]
350 fn summary_empty_nodes() {
351 let nodes: Vec<NodeSummary> = vec![];
352 let summary = calculate_group_hw_component_summary(&nodes);
353 assert!(summary.is_empty());
354 }
355
356 #[test]
357 fn summary_skips_none_info_in_processors() {
358 let nodes = vec![NodeSummary {
359 xname: "n1".to_string(),
360 processors: vec![
361 make_artifact(ArtifactType::Processor, None),
362 make_artifact(ArtifactType::Processor, Some("AMD EPYC 7742")),
363 ],
364 ..Default::default()
365 }];
366 let summary = calculate_group_hw_component_summary(&nodes);
367 assert_eq!(summary.get("AMD EPYC 7742"), Some(&1));
368 assert_eq!(summary.len(), 1);
369 }
370}