manta_server/service/
node_details.rs

1//! Per-xname `NodeDetails` aggregation built from the backend
2//! dispatcher.
3//!
4//! Replaces the direct `csm_rs::node::utils::get_node_details` call
5//! that previously lived in `service/cluster.rs` and `service/node.rs`.
6//! Going through the dispatcher's per-trait methods keeps the service
7//! layer backend-agnostic — both CSM and OCHAMI implement the
8//! underlying `CfsTrait` / `BootParametersTrait` / `ComponentTrait` /
9//! `GroupTrait` calls used here, so the function works on either
10//! site without a runtime branch.
11//!
12//! The flow is one round of five parallel fetches followed by an
13//! in-memory join keyed by xname, intentionally trading marginally
14//! more total bytes pulled (the CFS session list is unfiltered) for
15//! O(1) per-xname lookup and no N+1 per-node HSM call. The
16//! `csm_rs::node::utils::get_node_details` it replaces used a
17//! semaphore-bounded `JoinSet` to fan out one HSM-membership call per
18//! node; this version derives memberships from a single
19//! `get_groups(None)` instead.
20
21use std::collections::HashMap;
22
23use manta_backend_dispatcher::error::Error;
24use manta_backend_dispatcher::interfaces::{
25  bss::BootParametersTrait,
26  cfs::CfsTrait,
27  hsm::{component::ComponentTrait, group::GroupTrait},
28};
29use manta_shared::types::dto::NodeDetails;
30
31use crate::server::common::app_context::InfraContext;
32
33/// Fallback string used when a backend field is absent. Matches the
34/// historical csm-rs behavior so callers (CLI table renderer, status
35/// summary) don't need to special-case.
36const NOT_FOUND: &str = "Not found";
37
38/// Return one [`NodeDetails`] per xname in `xnames`.
39///
40/// Xnames that are present in `xnames` but missing from any one of
41/// the five backend responses still get a row; the affected fields
42/// are filled with `"Not found"` so the per-row position in the
43/// returned vector matches `xnames` after sorting.
44///
45/// The caller is expected to have already validated group access to
46/// every xname; this helper does no authorization of its own.
47pub async fn get_node_details(
48  infra: &InfraContext<'_>,
49  token: &str,
50  xnames: &[String],
51) -> Result<Vec<NodeDetails>, Error> {
52  // CFS components endpoint takes a comma-separated id filter; build
53  // it once. The other backends accept xname slices directly.
54  let xname_filter = xnames.join(",");
55
56  let (cfs_components, boot_params_vec, hsm_components, cfs_sessions, groups) =
57    tokio::try_join!(
58      infra
59        .backend
60        .get_cfs_components(token, None, Some(&xname_filter), None),
61      infra.backend.get_bootparameters(token, xnames),
62      infra.backend.get_node_metadata_available(token),
63      // Successful sessions only — we use them to resolve image id →
64      // CFS configuration that built the image.
65      infra.backend.get_sessions(
66        token,
67        None,
68        None,
69        None,
70        None,
71        None,
72        None,
73        None,
74        Some(true),
75        None
76      ),
77      infra.backend.get_groups(token, None),
78    )?;
79
80  // Build xname → comma-separated group label lookup once.
81  let mut xname_to_groups: HashMap<String, Vec<String>> = HashMap::new();
82  for group in &groups {
83    if let Some(member_ids) =
84      group.members.as_ref().and_then(|m| m.ids.as_ref())
85    {
86      for id in member_ids {
87        xname_to_groups
88          .entry(id.clone())
89          .or_default()
90          .push(group.label.clone());
91      }
92    }
93  }
94
95  // Index the per-xname lookups so the build loop below is O(N).
96  let cfs_by_id: HashMap<&str, &_> = cfs_components
97    .iter()
98    .filter_map(|c| c.id.as_deref().map(|id| (id, c)))
99    .collect();
100  let hsm_by_id: HashMap<&str, &_> = hsm_components
101    .iter()
102    .filter_map(|c| c.id.as_deref().map(|id| (id, c)))
103    .collect();
104
105  // Image id → CFS configuration name that produced it.
106  let image_to_cfs_config: HashMap<String, String> = cfs_sessions
107    .iter()
108    .filter_map(|session| {
109      let result_id = session.get_first_result_id()?;
110      let configuration_name = session.configuration.as_ref()?.name.as_ref()?;
111      Some((result_id, configuration_name.clone()))
112    })
113    .collect();
114
115  let mut out: Vec<NodeDetails> = xnames
116    .iter()
117    .map(|xname| {
118      let hsm_info = hsm_by_id.get(xname.as_str());
119      let nid = hsm_info
120        .and_then(|c| c.nid)
121        .map_or_else(|| NOT_FOUND.to_string(), |n| format!("nid{n:0>6}"));
122      let power_status = hsm_info
123        .and_then(|c| c.state.as_ref())
124        .map_or_else(|| NOT_FOUND.to_string(), |s| s.to_uppercase());
125
126      let cfs = cfs_by_id.get(xname.as_str());
127      let desired_configuration = cfs
128        .and_then(|c| c.desired_config.clone())
129        .unwrap_or_else(|| NOT_FOUND.to_string());
130      let configuration_status = cfs
131        .and_then(|c| c.configuration_status.clone())
132        .unwrap_or_else(|| NOT_FOUND.to_string());
133      let enabled = cfs
134        .and_then(|c| c.enabled)
135        .map_or_else(|| NOT_FOUND.to_string(), |b| b.to_string());
136      let error_count = cfs
137        .and_then(|c| c.error_count)
138        .map_or_else(|| NOT_FOUND.to_string(), |n| n.to_string());
139
140      let boot_params = boot_params_vec
141        .iter()
142        .find(|bp| bp.hosts.iter().any(|h| h == xname));
143      let (boot_image_id, kernel_params) = boot_params.map_or_else(
144        || (NOT_FOUND.to_string(), NOT_FOUND.to_string()),
145        |bp| {
146          (
147            bp.try_get_boot_image_id()
148              .unwrap_or_else(|| NOT_FOUND.to_string()),
149            bp.params.clone(),
150          )
151        },
152      );
153
154      let boot_configuration = image_to_cfs_config
155        .get(&boot_image_id)
156        .cloned()
157        .unwrap_or_else(|| NOT_FOUND.to_string());
158
159      let hsm = xname_to_groups
160        .get(xname)
161        .map(|labels| labels.join(", "))
162        .unwrap_or_default();
163
164      NodeDetails {
165        xname: xname.clone(),
166        nid,
167        hsm,
168        power_status,
169        desired_configuration,
170        configuration_status,
171        enabled,
172        error_count,
173        boot_image_id,
174        boot_configuration,
175        kernel_params,
176      }
177    })
178    .collect();
179
180  out.sort_by(|a, b| a.xname.cmp(&b.xname));
181
182  Ok(out)
183}