manta_server/server/common/
node_ops.rs1use std::collections::HashMap;
6use std::sync::LazyLock;
7
8use hostlist_parser::parse;
9use manta_backend_dispatcher::{
10 error::Error,
11 interfaces::hsm::{component::ComponentTrait, group::GroupTrait},
12 types::Component,
13};
14use regex::Regex;
15
16static XNAME_RE: LazyLock<Regex> = LazyLock::new(|| {
19 Regex::new(r"^x\d{4}c[0-7]s([0-9]|[1-5][0-9]|6[0-4])b[0-1]n[0-7]$")
20 .expect("Invalid xname regex pattern")
21});
22
23use crate::manta_backend_dispatcher::StaticBackendDispatcher;
24
25const NID_STRING_LENGTH: usize = 9;
27
28const XNAME_BLADE_PREFIX_LEN: usize = 10;
30
31fn get_short_nid(long_nid: &str) -> Result<usize, Error> {
33 if long_nid.len() != NID_STRING_LENGTH {
34 return Err(Error::InvalidNodeId(format!(
35 "Nid '{long_nid}' not valid, Nid does not have {NID_STRING_LENGTH} characters"
36 )));
37 }
38
39 let nid_number = long_nid.strip_prefix("nid").ok_or_else(|| {
40 Error::InvalidNodeId(format!(
41 "Nid '{long_nid}' not valid, 'nid' prefix missing"
42 ))
43 })?;
44
45 nid_number.parse::<usize>().map_err(|e| {
46 Error::InvalidNodeId(format!(
47 "Could not convert Nid '{nid_number}' from long to short format: {e}"
48 ))
49 })
50}
51
52pub async fn get_xname_from_nid_hostlist(
55 node_vec: &[String],
56 node_metadata_available_vec: &[Component],
57) -> Result<Vec<String>, Error> {
58 let short_nid_vec: Vec<usize> = node_vec
61 .iter()
62 .map(|nid_long| get_short_nid(nid_long))
63 .collect::<Result<Vec<_>, _>>()?;
64
65 tracing::debug!("short Nid list expanded: {:?}", short_nid_vec);
66
67 let xname_vec: Vec<String> = node_metadata_available_vec
68 .iter()
69 .filter(|node_metadata_available| {
70 node_metadata_available
71 .nid
72 .is_some_and(|nid| short_nid_vec.contains(&nid))
73 })
74 .filter_map(|node_metadata_available| {
75 node_metadata_available.id.as_ref().cloned()
76 })
77 .collect();
78
79 Ok(xname_vec)
80}
81
82pub async fn get_xname_from_xname_hostlist(
85 node_vec: &[String],
86 node_metadata_available_vec: &[Component],
87) -> Result<Vec<String>, Error> {
88 let xname_vec: Vec<String> = node_metadata_available_vec
91 .iter()
92 .filter(|node_metadata_available| {
93 node_metadata_available
94 .id
95 .as_ref()
96 .is_some_and(|id| node_vec.contains(id))
97 })
98 .filter_map(|node_metadata_available| {
99 node_metadata_available.id.as_ref().cloned()
100 })
101 .collect();
102
103 Ok(xname_vec)
104}
105
106pub async fn resolve_hosts_expression(
119 backend: &StaticBackendDispatcher,
120 shasta_token: &str,
121 hosts_expression: &str,
122 is_include_siblings: bool,
123) -> Result<Vec<String>, Error> {
124 let node_metadata_available_vec =
125 backend.get_node_metadata_available(shasta_token).await?;
126
127 let mut xname_vec = from_hosts_expression_to_xname_vec(
128 hosts_expression,
129 is_include_siblings,
130 node_metadata_available_vec,
131 )
132 .await?;
133
134 xname_vec.sort();
135 xname_vec.dedup();
136
137 Ok(xname_vec)
138}
139
140pub async fn from_hosts_expression_to_xname_vec(
144 user_input: &str,
145 is_include_siblings: bool,
146 node_metadata_available_vec: Vec<Component>,
147) -> Result<Vec<String>, Error> {
148 let hostlist_expanded_vec_rslt =
149 parse(user_input).map_err(|e| Error::InvalidNodeId(e.to_string()));
150
151 let xname_vec = match hostlist_expanded_vec_rslt {
152 Ok(node_vec) => {
153 tracing::debug!("Hostlist format is valid");
154 let xname_vec: Vec<String> = if validate_nid_format_vec(&node_vec) {
155 tracing::debug!("NID format is valid");
156 tracing::debug!("hostlist Nids: {}", user_input);
157 tracing::debug!("hostlist Nids expanded: {:?}", node_vec);
158
159 get_xname_from_nid_hostlist(&node_vec, &node_metadata_available_vec)
160 .await?
161 } else if validate_xname_format_vec(&node_vec) {
162 tracing::debug!("XNAME format is valid");
163 tracing::debug!("hostlist XNAMEs: {}", user_input);
164 tracing::debug!("hostlist XNAMEs expanded: {:?}", node_vec);
165
166 get_xname_from_xname_hostlist(&node_vec, &node_metadata_available_vec)
167 .await?
168 } else {
169 return Err(Error::BadRequest(
170 "Could not parse user input as a list of nodes from a hostlist expression."
171 .to_string(),
172 ));
173 };
174
175 xname_vec
176 }
177 Err(e) => {
178 return Err(Error::BadRequest(format!(
179 "Could not parse user input as a list of nodes from a hostlist or regex expression: {e}"
180 )));
181 }
182 };
183
184 if xname_vec.is_empty() {
185 return Err(Error::BadRequest(
186 "Could not parse user input as a list of nodes from a hostlist or regex expression."
187 .to_string(),
188 ));
189 }
190
191 let xname_vec: Vec<String> = if is_include_siblings {
193 tracing::debug!("Include siblings");
194 let xname_blade_vec: Vec<String> = xname_vec
195 .iter()
196 .map(|xname| {
197 xname
198 .get(0..XNAME_BLADE_PREFIX_LEN)
199 .unwrap_or(xname)
200 .to_string()
201 })
202 .collect();
203
204 tracing::debug!("XNAME blades:\n{:?}", xname_blade_vec);
205
206 node_metadata_available_vec
209 .into_iter()
210 .filter(|node_metadata_available| {
211 node_metadata_available.id.as_ref().is_some_and(|id| {
212 xname_blade_vec
213 .iter()
214 .any(|xname_blade| id.starts_with(xname_blade))
215 })
216 })
217 .filter_map(|node_metadata_available| node_metadata_available.id)
218 .collect()
219 } else {
220 xname_vec
221 };
222
223 Ok(xname_vec)
224}
225
226pub async fn get_curated_hsm_group_from_xname_hostlist(
229 backend: &StaticBackendDispatcher,
230 auth_token: &str,
231 xname_vec: &[String],
232) -> Result<HashMap<String, Vec<String>>, Error> {
233 let mut hsm_group_summary: HashMap<String, Vec<String>> = HashMap::new();
234
235 let hsm_name_available_vec =
236 backend.get_group_name_available(auth_token).await?;
237
238 let hsm_group_available_map = backend
239 .get_group_map_and_filter_by_group_vec(
240 auth_token,
241 &hsm_name_available_vec
242 .iter()
243 .map(String::as_str)
244 .collect::<Vec<&str>>(),
245 )
246 .await?;
247
248 for (hsm_name, hsm_members) in hsm_group_available_map {
250 let xname_filtered: Vec<String> = hsm_members
251 .iter()
252 .filter(|&xname| xname_vec.contains(xname))
253 .cloned()
254 .collect();
255 if !xname_filtered.is_empty() {
256 hsm_group_summary.insert(hsm_name, xname_filtered);
257 }
258 }
259
260 Ok(hsm_group_summary)
261}
262
263fn validate_nid_format_vec(node_vec: &[String]) -> bool {
264 node_vec.iter().all(|nid| validate_nid_format(nid))
265}
266
267fn validate_nid_format(nid: &str) -> bool {
268 nid.to_lowercase().starts_with("nid")
269 && nid.len() == 9
270 && nid
271 .strip_prefix("nid")
272 .is_some_and(|nid_number| nid_number.chars().all(char::is_numeric))
273}
274
275fn validate_xname_format_vec(node_vec: &[String]) -> bool {
276 node_vec.iter().all(|nid| validate_xname_format(nid))
277}
278
279pub fn validate_xname_format(xname: &str) -> bool {
281 XNAME_RE.is_match(xname)
282}
283
284pub async fn resolve_target_nodes(
303 backend: &StaticBackendDispatcher,
304 shasta_token: &str,
305 hosts_expression: Option<&str>,
306 hsm_group_name_arg_opt: Option<&str>,
307 settings_hsm_group_name_opt: Option<&str>,
308) -> Result<Vec<String>, Error> {
309 if let Some(hosts_expr) = hosts_expression {
310 resolve_hosts_expression(backend, shasta_token, hosts_expr, false).await
311 } else if hsm_group_name_arg_opt.is_some()
312 || settings_hsm_group_name_opt.is_some()
313 {
314 let hsm_group_name_vec =
315 crate::server::common::authorization::get_groups_names_available(
316 backend,
317 shasta_token,
318 hsm_group_name_arg_opt,
319 settings_hsm_group_name_opt,
320 )
321 .await?;
322
323 let hsm_members: Vec<String> = backend
324 .get_member_vec_from_group_name_vec(shasta_token, &hsm_group_name_vec)
325 .await?;
326
327 resolve_hosts_expression(
328 backend,
329 shasta_token,
330 &hsm_members.join(","),
331 false,
332 )
333 .await
334 } else {
335 Err(Error::BadRequest(
336 "No nodes provided. Please provide either a list of nodes \
337 via --nodes or an HSM group via --hsm-group"
338 .to_string(),
339 ))
340 }
341}
342
343#[cfg(test)]
344mod tests;