Skip to main content

csm_rs/node/
utils.rs

1use std::{collections::HashMap, sync::Arc, time::Instant};
2
3use regex::Regex;
4use serde_json::Value;
5use tokio::sync::Semaphore;
6
7use crate::{bss, cfs, error::Error, hsm};
8
9use super::types::NodeDetails;
10
11/// Validate user has access to a list of HSM group members provided.
12/// HSM members user is asking for are taken from cli command
13/// Exit if user does not have access to any of the members provided. By not having access to a HSM
14/// members means, the node belongs to an HSM group which the user does not have access
15pub async fn validate_target_hsm_members(
16  shasta_token: &str,
17  shasta_base_url: &str,
18  shasta_root_cert: &[u8],
19  socks5_proxy: Option<&str>,
20  hsm_group_members_opt: &[&str],
21) -> Result<Vec<String>, Error> {
22  let hsm_groups_user_has_access = hsm::group::utils::get_group_name_available(
23    shasta_token,
24    shasta_base_url,
25    shasta_root_cert,
26    socks5_proxy,
27  )
28  .await?;
29  /* let hsm_groups_user_has_access = config_show::get_hsm_name_available_from_jwt_or_all(
30      shasta_token,
31      shasta_base_url,
32      shasta_root_cert,
33  )
34  .await; */
35
36  let xnames_user_has_access =
37    hsm::group::utils::get_member_vec_from_hsm_name_vec(
38      shasta_token,
39      shasta_base_url,
40      shasta_root_cert,
41      socks5_proxy,
42      &hsm_groups_user_has_access,
43    )
44    .await?;
45  /* let all_xnames_user_has_access = hsm::group::utils::get_member_vec_from_hsm_name_vec(
46      shasta_token,
47      shasta_base_url,
48      shasta_root_cert,
49      hsm_groups_user_has_access.clone(),
50  )
51  .await; */
52
53  // Check user has access to all xnames he is requesting
54  if hsm_group_members_opt
55    .iter()
56    .all(|hsm_member| xnames_user_has_access.contains(&hsm_member.to_string()))
57  {
58    Ok(
59      hsm_group_members_opt
60        .as_ref()
61        .iter()
62        .cloned()
63        .map(str::to_string)
64        .collect(),
65    )
66  } else {
67    return Err(Error::Message(format!(
68      "Can't access all or any of the HSM members '{}'.\nPlease choose members form the list of HSM groups below:\n{}\nExit",
69      hsm_group_members_opt.join(", "),
70      hsm_groups_user_has_access.join(", ")
71    )));
72  }
73}
74
75/// Check if input is a NID
76pub fn validate_nid_format_regex(node_vec: Vec<String>, regex: Regex) -> bool {
77  node_vec.iter().all(|nid| regex.is_match(nid))
78}
79
80/// Check if input is a NID
81pub fn validate_nid_format_vec(node_vec: Vec<String>) -> bool {
82  node_vec.iter().all(|nid| validate_nid_format(nid))
83}
84
85/// Check if input is a NID
86pub fn validate_nid_format(nid: &str) -> bool {
87  nid.to_lowercase().starts_with("nid")
88    && nid.len() == 9
89    && nid
90      .strip_prefix("nid")
91      .is_some_and(|nid_number| nid_number.chars().all(char::is_numeric))
92}
93
94/// Validate xname is correct (it uses regex taken from HPE Cray CSM docs)
95pub fn validate_xname_format_regex(
96  node_vec: Vec<String>,
97  regex: Regex,
98) -> bool {
99  node_vec.iter().all(|nid| regex.is_match(nid))
100}
101
102/// Validate xname is correct (it uses regex taken from HPE Cray CSM docs)
103pub fn validate_xname_format_vec(node_vec: Vec<String>) -> bool {
104  node_vec.iter().all(|nid| validate_xname_format(nid))
105}
106
107/// Validate xname is correct (it uses regex taken from HPE Cray CSM docs)
108pub fn validate_xname_format(xname: &str) -> bool {
109  let xname_re =
110    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]$")
111      .unwrap();
112
113  xname_re.is_match(xname)
114}
115
116/// Validates a list of xnames.
117/// Checks xnames strings are valid
118/// If hsm_group_name_opt provided, then checks all xnames belongs to that hsm_group
119// TODO: idually, we should create a struct with the data available to the user, then operate with
120// it in memory, that way we avoid multiple calls to Shasta APIs
121pub async fn validate_xnames_format_and_membership_agaisnt_single_hsm(
122  shasta_token: &str,
123  shasta_base_url: &str,
124  shasta_root_cert: &[u8],
125  socks5_proxy: Option<&str>,
126  xnames: &[&str],
127  hsm_group_name_opt: Option<&str>,
128) -> Result<bool, Error> {
129  let hsm_group_members: Vec<String> =
130    if let Some(hsm_group_name) = hsm_group_name_opt {
131      hsm::group::utils::get_member_vec_from_hsm_group_name(
132        shasta_token,
133        shasta_base_url,
134        shasta_root_cert,
135        socks5_proxy,
136        hsm_group_name,
137      )
138      .await?
139    } else {
140      Vec::new()
141    };
142
143  if xnames.iter().any(|&xname| {
144    !validate_xname_format(xname)
145      || (!hsm_group_members.is_empty()
146        && !hsm_group_members.contains(&xname.to_string()))
147  }) {
148    return Ok(false);
149  }
150
151  Ok(true)
152}
153
154/// Get components data.
155/// Currently, CSM will throw an error if many xnames are sent in the request, therefore, this
156/// method will paralelize multiple calls, each with a batch of xnames
157pub async fn get_node_details(
158  shasta_token: &str,
159  shasta_base_url: &str,
160  shasta_root_cert: &[u8],
161  socks5_proxy: Option<&str>,
162  xname_list: Vec<String>,
163) -> Result<Vec<NodeDetails>, Error> {
164  let start = Instant::now();
165
166  let (
167    components_status_rslt,
168    node_boot_params_vec_rslt,
169    node_hsm_info_rslt,
170    cfs_session_vec_rslt,
171  ) = tokio::join!(
172    // Get CFS component status
173    cfs::component::http_client::v2::get_multiple(
174      shasta_token,
175      shasta_base_url,
176      shasta_root_cert,
177      socks5_proxy,
178      &xname_list,
179    ),
180    // Get boot params to get the boot image id for each node
181    bss::http_client::get_multiple(
182      shasta_token,
183      shasta_base_url,
184      shasta_root_cert,
185      socks5_proxy,
186      &xname_list,
187    ),
188    // Get HSM component status (needed to get NIDS)
189    hsm::component::http_client::get_and_filter(
190      shasta_token,
191      shasta_base_url,
192      shasta_root_cert,
193      socks5_proxy,
194      &xname_list,
195    ),
196    // Get CFS sessions
197    cfs::session::get_and_sort(
198      shasta_token,
199      shasta_base_url,
200      shasta_root_cert,
201      socks5_proxy,
202      None,
203      None,
204      None,
205      None,
206      Some(true),
207    )
208  );
209
210  let node_hsm_info = node_hsm_info_rslt?;
211  let node_boot_params_vec = node_boot_params_vec_rslt?;
212  let cfs_session_vec = cfs_session_vec_rslt?;
213
214  // ------------------------------------------------------------------------
215  // Get and collect HSM members
216  let mut node_details_map = HashMap::new();
217  let mut tasks = tokio::task::JoinSet::new();
218
219  let sem = Arc::new(Semaphore::new(10)); // CSM 1.3.1 higher number of concurrent tasks won't
220
221  for xname in xname_list {
222    let shasta_token_string = shasta_token.to_string();
223    let shasta_base_url_string = shasta_base_url.to_string();
224    let shasta_root_cert_vec = shasta_root_cert.to_vec();
225    let socks5_proxy_opt = socks5_proxy.map(str::to_owned);
226
227    let components_status = components_status_rslt.as_ref().unwrap();
228
229    // find component details
230    let component_details_opt = components_status
231      .iter()
232      .find(|component_status| component_status.id.as_ref().eq(&Some(&xname)));
233
234    // FIXME: fix this by converting 'compoennt_details_opt' into a Result, with
235    // backend-dispatcher::Error and resolve the value using '?'
236    let component_details =
237      if let Some(component_details) = component_details_opt {
238        component_details
239      } else {
240        return Err(Error::Message(format!(
241          "ERROR - CFS component details for node {}.\nReason:\n{:#?}",
242          xname, component_details_opt
243        )));
244      };
245
246    let desired_configuration = &component_details.desired_config;
247    let configuration_status = &component_details.configuration_status;
248    let enabled = component_details.enabled;
249    let error_count = component_details.error_count.clone();
250
251    // Get node HSM details
252    let node_hsm_info = node_hsm_info
253      .iter()
254      .find(|component| component.id.eq(&Some(xname.clone())))
255      .ok_or_else(|| Error::HsmComponentNotFound(xname.clone()))?;
256
257    let node_hsm_id = node_hsm_info
258      .id
259      .as_ref()
260      .ok_or_else(|| Error::HsmComponentIdNotDefined(xname.clone()))?;
261
262    // Get power status
263    let node_power_status = node_hsm_info
264      .state
265      .as_ref()
266      .ok_or_else(|| Error::HsmComponentPowerStateNotDefined(xname.clone()))?
267      .to_uppercase();
268
269    // Get NID
270    let nid = node_hsm_info
271      .nid
272      .ok_or_else(|| Error::HsmComponentNidNotDefined(node_hsm_id.clone()))?;
273
274    // Calculate NID
275    let node_nid = format!("nid{:0>6}", nid.to_string());
276
277    // get node boot params (these are the boot params of the nodes with the image the node
278    // boot with). the image in the bos sessiontemplate may be different i don't know why. need
279    // to investigate
280    let (image_id_in_kernel_params, kernel_params): (String, String) =
281      if let Some(node_boot_params) =
282        bss::utils::find_boot_params_related_to_node(
283          &node_boot_params_vec,
284          &xname,
285        )
286      {
287        (node_boot_params.get_boot_image(), node_boot_params.params)
288      } else {
289        eprintln!("BSS boot parameters for node '{}' - NOT FOUND", xname);
290        ("Not found".to_string(), "Not found".to_string())
291      };
292
293    // Get CFS configuration related to image id
294    let cfs_session_related_to_image_id_opt =
295      cfs::session::utils::find_cfs_session_related_to_image_id(
296        &cfs_session_vec,
297        &image_id_in_kernel_params,
298      );
299
300    let cfs_configuration_boot = if let Some(cfs_session_related_to_image_id) =
301      cfs_session_related_to_image_id_opt
302    {
303      let session_name = cfs_session_related_to_image_id.name;
304
305      cfs_session_related_to_image_id
306        .configuration
307        .ok_or_else(|| {
308          Error::SessionConfigurationNotDefined(session_name.clone())
309        })?
310        .name
311        .ok_or_else(|| {
312          Error::SessionConfigurationNotDefined(session_name.clone())
313        })?
314    } else {
315      "Not found".to_string()
316    };
317
318    node_details_map
319      .entry(xname.clone())
320      .and_modify(|node_details: &mut NodeDetails| {
321        node_details.xname = xname.clone();
322        node_details.nid = node_nid.clone();
323        node_details.hsm = "".to_string();
324        node_details.power_status = node_power_status.clone();
325        node_details.desired_configuration =
326          desired_configuration.clone().unwrap();
327        node_details.configuration_status =
328          configuration_status.clone().unwrap();
329        node_details.enabled = enabled.as_ref().map(bool::to_string).unwrap();
330        node_details.error_count =
331          error_count.as_ref().map(u64::to_string).unwrap();
332        node_details.boot_image_id = image_id_in_kernel_params.clone();
333        node_details.boot_configuration = cfs_configuration_boot.clone();
334        node_details.kernel_params = kernel_params.clone();
335      })
336      .or_insert(NodeDetails {
337        xname: xname.clone(),
338        nid: node_nid,
339        hsm: "".to_string(),
340        power_status: node_power_status,
341        desired_configuration: desired_configuration.clone().unwrap(),
342        configuration_status: configuration_status.clone().unwrap(),
343        enabled: enabled.as_ref().map(bool::to_string).unwrap(),
344        error_count: error_count.as_ref().map(u64::to_string).unwrap(),
345        boot_image_id: image_id_in_kernel_params,
346        boot_configuration: cfs_configuration_boot,
347        kernel_params,
348      });
349
350    let permit = Arc::clone(&sem).acquire_owned().await;
351
352    tasks.spawn(async move {
353      let _permit = permit; // Wait semaphore to allow new tasks https://github.com/tokio-rs/tokio/discussions/2648#discussioncomment-34885
354
355      hsm::memberships::http_client::get_xname(
356        &shasta_token_string,
357        &shasta_base_url_string,
358        &shasta_root_cert_vec,
359        socks5_proxy_opt.as_deref(),
360        &xname,
361      )
362      .await
363    });
364  }
365
366  while let Some(message) = tasks.join_next().await {
367    let node_membership = message??;
368
369    let node_details = NodeDetails {
370      xname: "".to_string(),
371      nid: "".to_string(),
372      hsm: node_membership.group_labels.join(", "),
373      power_status: "".to_string(),
374      desired_configuration: "".to_string(),
375      configuration_status: "".to_string(),
376      enabled: "".to_string(),
377      error_count: "".to_string(),
378      boot_image_id: "".to_string(),
379      boot_configuration: "".to_string(),
380      kernel_params: "".to_string(),
381    };
382
383    node_details_map
384      .entry(node_membership.id.clone())
385      .and_modify(|node_details: &mut NodeDetails| {
386        node_details.hsm = node_membership.group_labels.join(", ")
387      })
388      .or_insert(node_details);
389  }
390
391  let duration = start.elapsed();
392  log::info!("Time elapsed to get node details is: {:?}", duration);
393  // ------------------------------------------------------------------------
394
395  Ok(node_details_map.into_values().collect())
396}
397
398pub fn nodes_to_string_format_one_line(nodes: Option<&Vec<Value>>) -> String {
399  if let Some(nodes_content) = nodes {
400    nodes_to_string_format_discrete_columns(nodes, nodes_content.len() + 1)
401  } else {
402    "".to_string()
403  }
404}
405
406pub fn nodes_to_string_format_discrete_columns(
407  nodes: Option<&Vec<Value>>,
408  num_columns: usize,
409) -> String {
410  let mut members: String;
411
412  match nodes {
413    Some(nodes) if !nodes.is_empty() => {
414      members = nodes
415        .first()
416        .and_then(Value::as_str)
417        .map(str::to_string)
418        .unwrap(); // take first element
419
420      for (i, _) in nodes.iter().enumerate().skip(1) {
421        // iterate for the rest of the list
422        if i % num_columns == 0 {
423          // breaking the cell content into multiple lines (only 2 xnames per line)
424
425          members.push_str(",\n");
426        } else {
427          members.push(',');
428        }
429
430        members.push_str(nodes.get(i).and_then(Value::as_str).unwrap());
431      }
432    }
433    _ => members = "".to_string(),
434  }
435
436  members
437}