use std::collections::HashMap;
use crate::shared::dto::NodeDetails;
use manta_backend_dispatcher::types::NodeSummary;
const MIB_PER_GIB: usize = 1024;
pub fn compute_summary_status(nodes: &[NodeDetails]) -> &'static str {
if nodes
.iter()
.any(|n| n.configuration_status.eq_ignore_ascii_case("failed"))
{
"FAILED"
} else if nodes
.iter()
.any(|n| n.power_status.eq_ignore_ascii_case("OFF"))
{
"OFF"
} else if nodes
.iter()
.any(|n| n.power_status.eq_ignore_ascii_case("on"))
{
"ON"
} else if nodes
.iter()
.any(|n| n.power_status.eq_ignore_ascii_case("standby"))
{
"STANDBY"
} else if nodes
.iter()
.any(|n| !n.configuration_status.eq_ignore_ascii_case("configured"))
{
"UNCONFIGURED"
} else {
"OK"
}
}
pub fn calculate_hsm_hw_component_summary(
node_summary_vec: &[NodeSummary],
) -> HashMap<String, usize> {
let mut node_hw_component_summary: HashMap<String, usize> = HashMap::new();
for node_summary in node_summary_vec {
for artifact_summary in &node_summary.processors {
if let Some(info) = artifact_summary.info.as_ref() {
node_hw_component_summary
.entry(info.to_string())
.and_modify(|qty| *qty += 1)
.or_insert(1);
}
}
for artifact_summary in &node_summary.node_accels {
if let Some(info) = artifact_summary.info.as_ref() {
node_hw_component_summary
.entry(info.to_string())
.and_modify(|qty| *qty += 1)
.or_insert(1);
}
}
for artifact_summary in &node_summary.memory {
let memory_capacity = artifact_summary
.info
.as_deref()
.unwrap_or("ERROR NA")
.split(' ')
.collect::<Vec<_>>()
.first()
.copied()
.unwrap_or("0")
.parse::<usize>()
.unwrap_or(0);
node_hw_component_summary
.entry(artifact_summary.r#type.to_string() + " (GiB)")
.and_modify(|qty| *qty += memory_capacity / MIB_PER_GIB)
.or_insert(memory_capacity / MIB_PER_GIB);
}
for artifact_summary in &node_summary.node_hsn_nics {
if let Some(info) = artifact_summary.info.as_ref() {
node_hw_component_summary
.entry(info.to_string())
.and_modify(|qty| *qty += 1)
.or_insert(1);
}
}
}
node_hw_component_summary
}
pub fn get_cluster_hw_pattern(
hsm_summary: Vec<NodeSummary>,
) -> HashMap<String, usize> {
let mut hsm_node_hw_component_count_hashmap: HashMap<String, usize> =
HashMap::new();
for node_summary in hsm_summary {
for processor in node_summary.processors {
if let Some(info) = processor.info {
hsm_node_hw_component_count_hashmap
.entry(info.chars().filter(|c| !c.is_whitespace()).collect())
.and_modify(|qty| *qty += 1)
.or_insert(1);
}
}
for node_accel in node_summary.node_accels {
if let Some(info) = node_accel.info {
hsm_node_hw_component_count_hashmap
.entry(info.chars().filter(|c| !c.is_whitespace()).collect())
.and_modify(|qty| *qty += 1)
.or_insert(1);
}
}
for memory_dimm in node_summary.memory {
let memory_capacity = memory_dimm
.info
.unwrap_or_else(|| "0".to_string())
.split(' ')
.next()
.unwrap_or("0")
.to_string()
.parse::<usize>()
.unwrap_or(0);
hsm_node_hw_component_count_hashmap
.entry("memory".to_string())
.and_modify(|qty| *qty += memory_capacity)
.or_insert(memory_capacity);
}
}
hsm_node_hw_component_count_hashmap
}
#[cfg(test)]
mod tests {
use super::*;
use manta_backend_dispatcher::types::{ArtifactSummary, ArtifactType};
fn node(power: &str, config: &str) -> NodeDetails {
NodeDetails {
xname: String::new(),
nid: String::new(),
hsm: String::new(),
power_status: power.to_string(),
desired_configuration: String::new(),
configuration_status: config.to_string(),
enabled: String::new(),
error_count: String::new(),
boot_image_id: String::new(),
boot_configuration: String::new(),
kernel_params: String::new(),
}
}
fn artifact(kind: ArtifactType, info: Option<&str>) -> ArtifactSummary {
ArtifactSummary {
xname: String::new(),
r#type: kind,
info: info.map(String::from),
}
}
fn summary(
processors: Vec<ArtifactSummary>,
memory: Vec<ArtifactSummary>,
accels: Vec<ArtifactSummary>,
nics: Vec<ArtifactSummary>,
) -> NodeSummary {
NodeSummary {
xname: String::new(),
r#type: String::new(),
processors,
memory,
node_accels: accels,
node_hsn_nics: nics,
}
}
#[test]
fn summary_status_failed_beats_everything() {
let nodes = [
node("ON", "failed"),
node("OFF", "configured"),
node("on", "configured"),
];
assert_eq!(compute_summary_status(&nodes), "FAILED");
}
#[test]
fn summary_status_off_beats_on() {
let nodes = [node("OFF", "configured"), node("on", "configured")];
assert_eq!(compute_summary_status(&nodes), "OFF");
}
#[test]
fn summary_status_on_beats_standby() {
let nodes = [node("on", "configured"), node("standby", "configured")];
assert_eq!(compute_summary_status(&nodes), "ON");
}
#[test]
fn summary_status_standby_beats_unconfigured() {
let nodes = [node("standby", "configured"), node("ready", "pending")];
assert_eq!(compute_summary_status(&nodes), "STANDBY");
}
#[test]
fn summary_status_unconfigured_when_only_config_differs() {
let nodes = [node("ready", "pending")];
assert_eq!(compute_summary_status(&nodes), "UNCONFIGURED");
}
#[test]
fn summary_status_ok_when_all_configured_and_no_known_power_state() {
let nodes = [node("ready", "configured"), node("ready", "configured")];
assert_eq!(compute_summary_status(&nodes), "OK");
}
#[test]
fn summary_status_empty_input_is_ok() {
assert_eq!(compute_summary_status(&[]), "OK");
}
#[test]
fn summary_status_matches_case_insensitively() {
assert_eq!(
compute_summary_status(&[node("off", "configured")]),
"OFF"
);
assert_eq!(
compute_summary_status(&[node("ON", "CONFIGURED")]),
"ON"
);
}
#[test]
fn hw_summary_empty_input_is_empty() {
assert!(calculate_hsm_hw_component_summary(&[]).is_empty());
}
#[test]
fn hw_summary_counts_identical_processors_across_nodes() {
let node_a = summary(
vec![
artifact(ArtifactType::Processor, Some("AMD EPYC 7763")),
artifact(ArtifactType::Processor, Some("AMD EPYC 7763")),
],
vec![],
vec![],
vec![],
);
let node_b = summary(
vec![artifact(ArtifactType::Processor, Some("AMD EPYC 7763"))],
vec![],
vec![],
vec![],
);
let got = calculate_hsm_hw_component_summary(&[node_a, node_b]);
assert_eq!(got.get("AMD EPYC 7763"), Some(&3));
}
#[test]
fn hw_summary_converts_memory_mib_to_gib() {
let node = summary(
vec![],
vec![artifact(ArtifactType::Memory, Some("524288 MiB"))],
vec![],
vec![],
);
let got = calculate_hsm_hw_component_summary(&[node]);
assert_eq!(got.get("Memory (GiB)"), Some(&512));
}
#[test]
fn hw_summary_skips_artifacts_with_no_info_field() {
let node = summary(
vec![artifact(ArtifactType::Processor, None)],
vec![],
vec![artifact(ArtifactType::NodeAccel, None)],
vec![artifact(ArtifactType::NodeHsnNic, None)],
);
assert!(calculate_hsm_hw_component_summary(&[node]).is_empty());
}
#[test]
fn hw_summary_treats_unparseable_memory_as_zero() {
let node = summary(
vec![],
vec![artifact(ArtifactType::Memory, Some("garbage"))],
vec![],
vec![],
);
let got = calculate_hsm_hw_component_summary(&[node]);
assert_eq!(got.get("Memory (GiB)"), Some(&0));
}
#[test]
fn hw_pattern_empty_input_is_empty() {
assert!(get_cluster_hw_pattern(vec![]).is_empty());
}
#[test]
fn hw_pattern_strips_whitespace_from_processor_info() {
let node = summary(
vec![artifact(ArtifactType::Processor, Some("AMD EPYC 7763"))],
vec![],
vec![],
vec![],
);
let got = get_cluster_hw_pattern(vec![node]);
assert_eq!(got.get("AMDEPYC7763"), Some(&1));
assert!(
got.get("AMD EPYC 7763").is_none(),
"whitespace-bearing key must NOT be present"
);
}
#[test]
fn hw_pattern_aggregates_memory_as_raw_value_not_gib() {
let node = summary(
vec![],
vec![artifact(ArtifactType::Memory, Some("512 MiB"))],
vec![],
vec![],
);
let got = get_cluster_hw_pattern(vec![node]);
assert_eq!(got.get("memory"), Some(&512));
}
}