manta_server/service/
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::server::common::app_context::InfraContext;
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 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 short_nid_set: std::collections::HashSet<usize> =
72 short_nid_vec.iter().copied().collect();
73 let xname_vec: Vec<String> = node_metadata_available_vec
74 .iter()
75 .filter(|node_metadata_available| {
76 node_metadata_available
77 .nid
78 .is_some_and(|nid| short_nid_set.contains(&nid))
79 })
80 .filter_map(|node_metadata_available| {
81 node_metadata_available.id.as_ref().cloned()
82 })
83 .collect();
84
85 Ok(xname_vec)
86}
87
88pub fn get_xname_from_xname_hostlist(
91 node_vec: &[String],
92 node_metadata_available_vec: &[Component],
93) -> Result<Vec<String>, Error> {
94 let node_set: std::collections::HashSet<&str> =
101 node_vec.iter().map(String::as_str).collect();
102 let xname_vec: Vec<String> = node_metadata_available_vec
103 .iter()
104 .filter(|node_metadata_available| {
105 node_metadata_available
106 .id
107 .as_ref()
108 .is_some_and(|id| node_set.contains(id.as_str()))
109 })
110 .filter_map(|node_metadata_available| {
111 node_metadata_available.id.as_ref().cloned()
112 })
113 .collect();
114
115 Ok(xname_vec)
116}
117
118pub async fn from_user_hosts_expression_to_xname_vec(
128 infra: &InfraContext<'_>,
129 shasta_token: &str,
130 hosts_expression: &str,
131 is_include_siblings: bool,
132) -> Result<Vec<String>, Error> {
133 let node_metadata_available_vec = infra
134 .backend
135 .get_node_metadata_available(shasta_token)
136 .await?;
137
138 let mut xname_vec = from_hosts_expression_to_xname_vec(
139 hosts_expression,
140 is_include_siblings,
141 &node_metadata_available_vec,
142 )?;
143
144 xname_vec.sort();
145 xname_vec.dedup();
146
147 Ok(xname_vec)
148}
149
150pub fn from_hosts_expression_to_xname_vec(
156 user_input: &str,
157 is_include_siblings: bool,
158 node_metadata_available_vec: &[Component],
159) -> Result<Vec<String>, Error> {
160 let hostlist_expanded_vec_rslt =
161 parse(user_input).map_err(|e| Error::InvalidNodeId(e.to_string()));
162
163 let xname_vec = match hostlist_expanded_vec_rslt {
164 Ok(node_vec) => {
165 tracing::debug!("Hostlist format is valid");
166 let xname_vec: Vec<String> = if validate_nid_format_vec(&node_vec) {
167 tracing::debug!("NID format is valid");
168 tracing::debug!("hostlist Nids: {}", user_input);
169 tracing::debug!("hostlist Nids expanded: {:?}", node_vec);
170
171 get_xname_from_nid_hostlist(&node_vec, node_metadata_available_vec)?
172 } else if validate_xname_format_vec(&node_vec) {
173 tracing::debug!("XNAME format is valid");
174 tracing::debug!("hostlist XNAMEs: {}", user_input);
175 tracing::debug!("hostlist XNAMEs expanded: {:?}", node_vec);
176
177 get_xname_from_xname_hostlist(&node_vec, node_metadata_available_vec)?
178 } else {
179 return Err(Error::BadRequest(
180 "Could not parse user input as a list of nodes from a hostlist expression."
181 .to_string(),
182 ));
183 };
184
185 xname_vec
186 }
187 Err(e) => {
188 return Err(Error::BadRequest(format!(
189 "Could not parse user input as a list of nodes from a hostlist or regex expression: {e}"
190 )));
191 }
192 };
193
194 if xname_vec.is_empty() {
195 return Err(Error::BadRequest(
196 "Could not parse user input as a list of nodes from a hostlist or regex expression."
197 .to_string(),
198 ));
199 }
200
201 let xname_vec: Vec<String> = if is_include_siblings {
203 tracing::debug!("Include siblings");
204 let xname_blade_vec: Vec<String> = xname_vec
205 .iter()
206 .map(|xname| {
207 xname
208 .get(0..XNAME_BLADE_PREFIX_LEN)
209 .unwrap_or(xname)
210 .to_string()
211 })
212 .collect();
213
214 tracing::debug!("XNAME blades:\n{:?}", xname_blade_vec);
215
216 node_metadata_available_vec
219 .iter()
220 .filter(|node_metadata_available| {
221 node_metadata_available.id.as_ref().is_some_and(|id| {
222 xname_blade_vec
223 .iter()
224 .any(|xname_blade| id.starts_with(xname_blade))
225 })
226 })
227 .filter_map(|node_metadata_available| node_metadata_available.id.as_ref())
228 .cloned()
229 .collect()
230 } else {
231 xname_vec
232 };
233
234 Ok(xname_vec)
235}
236
237pub async fn get_curated_group_from_xname_hostlist(
245 infra: &InfraContext<'_>,
246 auth_token: &str,
247 xname_vec: &[String],
248) -> Result<HashMap<String, Vec<String>>, Error> {
249 let mut hsm_group_summary: HashMap<String, Vec<String>> = HashMap::new();
250
251 let hsm_name_available_vec =
252 infra.backend.get_group_name_available(auth_token).await?;
253
254 let names_ref: Vec<&str> =
255 hsm_name_available_vec.iter().map(String::as_str).collect();
256 let hsm_group_available_map = infra
257 .backend
258 .get_group_map_and_filter_by_group_vec(auth_token, &names_ref)
259 .await?;
260
261 let xname_set: std::collections::HashSet<&str> =
266 xname_vec.iter().map(String::as_str).collect();
267 for (hsm_name, hsm_members) in hsm_group_available_map {
268 let xname_filtered: Vec<String> = hsm_members
269 .iter()
270 .filter(|xname| xname_set.contains(xname.as_str()))
271 .cloned()
272 .collect();
273 if !xname_filtered.is_empty() {
274 hsm_group_summary.insert(hsm_name, xname_filtered);
275 }
276 }
277
278 Ok(hsm_group_summary)
279}
280
281fn validate_nid_format_vec(node_vec: &[String]) -> bool {
282 node_vec.iter().all(|nid| validate_nid_format(nid))
283}
284
285fn validate_nid_format(nid: &str) -> bool {
286 nid.to_lowercase().starts_with("nid")
287 && nid.len() == 9
288 && nid
289 .strip_prefix("nid")
290 .is_some_and(|nid_number| nid_number.chars().all(char::is_numeric))
291}
292
293fn validate_xname_format_vec(node_vec: &[String]) -> bool {
294 node_vec.iter().all(|nid| validate_xname_format(nid))
295}
296
297pub(crate) fn validate_xname_format(xname: &str) -> bool {
299 XNAME_RE.is_match(xname)
300}
301
302pub async fn resolve_target_nodes(
318 infra: &InfraContext<'_>,
319 token: &str,
320 hosts_expression_opt: Option<&str>,
321 group_name_arg_opt: Option<&str>,
322 settings_group_name_opt: Option<&str>,
323) -> Result<Vec<String>, Error> {
324 if let Some(hosts_expr) = hosts_expression_opt {
325 from_user_hosts_expression_to_xname_vec(infra, token, hosts_expr, false)
326 .await
327 } else if let Some(target_group) =
328 group_name_arg_opt.or(settings_group_name_opt)
329 {
330 crate::service::authorization::validate_user_group_access(
331 infra,
332 token,
333 target_group,
334 )
335 .await?;
336
337 infra
338 .backend
339 .get_member_vec_from_group_name_vec(token, &[target_group.to_string()])
340 .await
341 } else {
342 Err(Error::BadRequest(
343 "No nodes provided. Please provide either a list of nodes \
344 via --nodes or an HSM group via --hsm-group"
345 .to_string(),
346 ))
347 }
348}
349
350#[cfg(test)]
351mod tests;