1use std::collections::HashMap;
9
10use crate::types::dto::NodeDetails;
11use manta_backend_dispatcher::types::NodeSummary;
12
13const MIB_PER_GIB: usize = 1024;
15
16pub fn compute_summary_status(nodes: &[NodeDetails]) -> &'static str {
57 if nodes
58 .iter()
59 .any(|n| n.configuration_status.eq_ignore_ascii_case("failed"))
60 {
61 "FAILED"
62 } else if nodes
63 .iter()
64 .any(|n| n.power_status.eq_ignore_ascii_case("OFF"))
65 {
66 "OFF"
67 } else if nodes
68 .iter()
69 .any(|n| n.power_status.eq_ignore_ascii_case("on"))
70 {
71 "ON"
72 } else if nodes
73 .iter()
74 .any(|n| n.power_status.eq_ignore_ascii_case("standby"))
75 {
76 "STANDBY"
77 } else if nodes
78 .iter()
79 .any(|n| !n.configuration_status.eq_ignore_ascii_case("configured"))
80 {
81 "UNCONFIGURED"
82 } else {
83 "OK"
84 }
85}
86
87pub fn calculate_group_hw_component_summary(
100 node_summary_vec: &[NodeSummary],
101) -> HashMap<String, usize> {
102 let mut node_hw_component_summary: HashMap<String, usize> = HashMap::new();
103
104 for node_summary in node_summary_vec {
105 for artifact_summary in &node_summary.processors {
106 if let Some(info) = artifact_summary.info.as_ref() {
107 node_hw_component_summary
108 .entry(info.clone())
109 .and_modify(|qty| *qty += 1)
110 .or_insert(1);
111 }
112 }
113 for artifact_summary in &node_summary.node_accels {
114 if let Some(info) = artifact_summary.info.as_ref() {
115 node_hw_component_summary
116 .entry(info.clone())
117 .and_modify(|qty| *qty += 1)
118 .or_insert(1);
119 }
120 }
121 for artifact_summary in &node_summary.memory {
122 let memory_capacity = artifact_summary
123 .info
124 .as_deref()
125 .unwrap_or("ERROR NA")
126 .split(' ')
127 .collect::<Vec<_>>()
128 .first()
129 .copied()
130 .unwrap_or("0")
131 .parse::<usize>()
132 .unwrap_or(0);
133 node_hw_component_summary
134 .entry(artifact_summary.r#type.to_string() + " (GiB)")
135 .and_modify(|qty| *qty += memory_capacity / MIB_PER_GIB)
136 .or_insert(memory_capacity / MIB_PER_GIB);
137 }
138 for artifact_summary in &node_summary.node_hsn_nics {
139 if let Some(info) = artifact_summary.info.as_ref() {
140 node_hw_component_summary
141 .entry(info.clone())
142 .and_modify(|qty| *qty += 1)
143 .or_insert(1);
144 }
145 }
146 }
147
148 node_hw_component_summary
149}
150
151pub fn get_cluster_hw_pattern(
166 hsm_summary: Vec<NodeSummary>,
167) -> HashMap<String, usize> {
168 let mut hsm_node_hw_component_count_hashmap: HashMap<String, usize> =
169 HashMap::new();
170
171 for node_summary in hsm_summary {
172 for processor in node_summary.processors {
173 if let Some(info) = processor.info {
174 hsm_node_hw_component_count_hashmap
175 .entry(info.chars().filter(|c| !c.is_whitespace()).collect())
176 .and_modify(|qty| *qty += 1)
177 .or_insert(1);
178 }
179 }
180
181 for node_accel in node_summary.node_accels {
182 if let Some(info) = node_accel.info {
183 hsm_node_hw_component_count_hashmap
184 .entry(info.chars().filter(|c| !c.is_whitespace()).collect())
185 .and_modify(|qty| *qty += 1)
186 .or_insert(1);
187 }
188 }
189
190 for memory_dimm in node_summary.memory {
191 let memory_capacity = memory_dimm
192 .info
193 .unwrap_or_else(|| "0".to_string())
194 .split(' ')
195 .next()
196 .unwrap_or("0")
197 .to_string()
198 .parse::<usize>()
199 .unwrap_or(0);
200
201 hsm_node_hw_component_count_hashmap
202 .entry("memory".to_string())
203 .and_modify(|qty| *qty += memory_capacity)
204 .or_insert(memory_capacity);
205 }
206 }
207
208 hsm_node_hw_component_count_hashmap
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use manta_backend_dispatcher::types::{ArtifactSummary, ArtifactType};
215
216 fn node(power: &str, config: &str) -> NodeDetails {
219 NodeDetails {
220 xname: String::new(),
221 nid: String::new(),
222 hsm: String::new(),
223 power_status: power.to_string(),
224 desired_configuration: String::new(),
225 configuration_status: config.to_string(),
226 enabled: String::new(),
227 error_count: String::new(),
228 boot_image_id: String::new(),
229 boot_configuration: String::new(),
230 kernel_params: String::new(),
231 }
232 }
233
234 fn artifact(kind: ArtifactType, info: Option<&str>) -> ArtifactSummary {
235 ArtifactSummary {
236 xname: String::new(),
237 r#type: kind,
238 info: info.map(String::from),
239 }
240 }
241
242 fn summary(
243 processors: Vec<ArtifactSummary>,
244 memory: Vec<ArtifactSummary>,
245 accels: Vec<ArtifactSummary>,
246 nics: Vec<ArtifactSummary>,
247 ) -> NodeSummary {
248 NodeSummary {
249 xname: String::new(),
250 r#type: String::new(),
251 processors,
252 memory,
253 node_accels: accels,
254 node_hsn_nics: nics,
255 }
256 }
257
258 #[test]
267 fn summary_status_failed_beats_everything() {
268 let nodes = [
269 node("ON", "failed"),
270 node("OFF", "configured"),
271 node("on", "configured"),
272 ];
273 assert_eq!(compute_summary_status(&nodes), "FAILED");
274 }
275
276 #[test]
277 fn summary_status_off_beats_on() {
278 let nodes = [node("OFF", "configured"), node("on", "configured")];
279 assert_eq!(compute_summary_status(&nodes), "OFF");
280 }
281
282 #[test]
283 fn summary_status_on_beats_standby() {
284 let nodes = [node("on", "configured"), node("standby", "configured")];
285 assert_eq!(compute_summary_status(&nodes), "ON");
286 }
287
288 #[test]
289 fn summary_status_standby_beats_unconfigured() {
290 let nodes = [node("standby", "configured"), node("ready", "pending")];
291 assert_eq!(compute_summary_status(&nodes), "STANDBY");
292 }
293
294 #[test]
295 fn summary_status_unconfigured_when_only_config_differs() {
296 let nodes = [node("ready", "pending")];
297 assert_eq!(compute_summary_status(&nodes), "UNCONFIGURED");
298 }
299
300 #[test]
301 fn summary_status_ok_when_all_configured_and_no_known_power_state() {
302 let nodes = [node("ready", "configured"), node("ready", "configured")];
303 assert_eq!(compute_summary_status(&nodes), "OK");
304 }
305
306 #[test]
307 fn summary_status_empty_input_is_ok() {
308 assert_eq!(compute_summary_status(&[]), "OK");
311 }
312
313 #[test]
314 fn summary_status_matches_case_insensitively() {
315 assert_eq!(compute_summary_status(&[node("off", "configured")]), "OFF");
317 assert_eq!(compute_summary_status(&[node("ON", "CONFIGURED")]), "ON");
318 }
319
320 #[test]
323 fn hw_summary_empty_input_is_empty() {
324 assert!(calculate_group_hw_component_summary(&[]).is_empty());
325 }
326
327 #[test]
328 fn hw_summary_counts_identical_processors_across_nodes() {
329 let node_a = summary(
330 vec![
331 artifact(ArtifactType::Processor, Some("AMD EPYC 7763")),
332 artifact(ArtifactType::Processor, Some("AMD EPYC 7763")),
333 ],
334 vec![],
335 vec![],
336 vec![],
337 );
338 let node_b = summary(
339 vec![artifact(ArtifactType::Processor, Some("AMD EPYC 7763"))],
340 vec![],
341 vec![],
342 vec![],
343 );
344 let got = calculate_group_hw_component_summary(&[node_a, node_b]);
345 assert_eq!(got.get("AMD EPYC 7763"), Some(&3));
346 }
347
348 #[test]
349 fn hw_summary_converts_memory_mib_to_gib() {
350 let node = summary(
352 vec![],
353 vec![artifact(ArtifactType::Memory, Some("524288 MiB"))],
354 vec![],
355 vec![],
356 );
357 let got = calculate_group_hw_component_summary(&[node]);
358 assert_eq!(got.get("Memory (GiB)"), Some(&512));
359 }
360
361 #[test]
362 fn hw_summary_skips_artifacts_with_no_info_field() {
363 let node = summary(
365 vec![artifact(ArtifactType::Processor, None)],
366 vec![],
367 vec![artifact(ArtifactType::NodeAccel, None)],
368 vec![artifact(ArtifactType::NodeHsnNic, None)],
369 );
370 assert!(calculate_group_hw_component_summary(&[node]).is_empty());
371 }
372
373 #[test]
374 fn hw_summary_treats_unparseable_memory_as_zero() {
375 let node = summary(
379 vec![],
380 vec![artifact(ArtifactType::Memory, Some("garbage"))],
381 vec![],
382 vec![],
383 );
384 let got = calculate_group_hw_component_summary(&[node]);
385 assert_eq!(got.get("Memory (GiB)"), Some(&0));
386 }
387
388 #[test]
391 fn hw_pattern_empty_input_is_empty() {
392 assert!(get_cluster_hw_pattern(vec![]).is_empty());
393 }
394
395 #[test]
396 fn hw_pattern_strips_whitespace_from_processor_info() {
397 let node = summary(
398 vec![artifact(ArtifactType::Processor, Some("AMD EPYC 7763"))],
399 vec![],
400 vec![],
401 vec![],
402 );
403 let got = get_cluster_hw_pattern(vec![node]);
404 assert_eq!(got.get("AMDEPYC7763"), Some(&1));
405 assert!(
406 !got.contains_key("AMD EPYC 7763"),
407 "whitespace-bearing key must NOT be present"
408 );
409 }
410
411 #[test]
412 fn hw_pattern_aggregates_memory_as_raw_value_not_gib() {
413 let node = summary(
418 vec![],
419 vec![artifact(ArtifactType::Memory, Some("512 MiB"))],
420 vec![],
421 vec![],
422 );
423 let got = get_cluster_hw_pattern(vec![node]);
424 assert_eq!(got.get("memory"), Some(&512));
425 }
426}