csm-rs 0.99.0

A library for Shasta
Documentation
use crate::{
  cfs,
  error::Error,
  hsm::group::{
    hacks::{filter_roles_and_subroles, filter_system_hsm_group_names},
    types::Group,
  },
};
use std::io::{self, Write};

use super::http_client::v2::types::CfsSessionGetResponse;
use globset::Glob;

// Check if a session is related to a group the user has access to
pub fn check_cfs_session_against_groups_available(
  cfs_session: &CfsSessionGetResponse,
  group_available: Vec<Group>,
) -> bool {
  group_available.iter().any(|group| {
    cfs_session
      .get_target_hsm()
      .is_some_and(|group_vec| group_vec.contains(&group.label))
      || cfs_session
        .get_target_xname()
        .is_some_and(|session_xname_vec| {
          session_xname_vec
            .iter()
            .all(|xname| group.get_members().contains(xname))
        })
  })
}

/// Checks if a session is "generic". A generic "session" is a session used to create an
/// image and it is not dedicated to a specific group.
/// Images generated by "generic sessions" are called generic images and they can be used by
/// any group.
/// For a CFS session to be generic, it must be linked to a system wide HSM group or a role or a subrole or xname related to a HSM group the user has access to.
/// Generic CFS sessions must belong to one or more of the following roles:
/// - Application
/// - Compute
/// - UAN
/// - UserDefined
/// - etc
/// How to calculate.
/// 1) We extract the HSM groups the CFS session is linked to
/// 2) We substract the roles mentioned above to the list of HSM groups
/// 3) We substract the system wide HSM groups
/// 4) If the list is empty then, we are dealing with a generic CFS session
pub fn is_session_image_generic(cfs_session: &CfsSessionGetResponse) -> bool {
  let target_group_vec_opt = cfs_session.get_target_hsm();
  let is_image = cfs_session.is_target_def_image();

  if let (Some(mut target_group_vec), true) = (target_group_vec_opt, is_image) {
    // Remove roles and subroles from the list of HSM groups
    target_group_vec = filter_roles_and_subroles(
      &target_group_vec
        .iter()
        .map(String::as_str)
        .collect::<Vec<&str>>(),
    );

    // Remove system wide HSM groups from the list of HSM groups
    target_group_vec = filter_system_hsm_group_names(target_group_vec);

    log::debug!(
      "CFS session {} is generic: {}",
      &cfs_session.name,
      target_group_vec.is_empty()
    );

    target_group_vec.is_empty()
  } else {
    false
  }
}

pub fn filter(
  cfs_session_vec: &mut Vec<CfsSessionGetResponse>,
  configuration_name_pattern_opt: Option<&str>,
  hsm_group_name_available_vec: &[String],
  xname_available_vec: &[String],
  type_opt: Option<&String>,
  limit_number_opt: Option<&u8>,
  keep_generic_sessions: bool,
) -> Result<(), Error> {
  log::info!("Filter CFS sessions by groups");
  log::info!(
    "HSM groups to filter from: {:?}",
    hsm_group_name_available_vec
  );
  log::debug!("Xnames to filter from: {:?}", xname_available_vec);

  if let Some(configuration_name_pattern) = configuration_name_pattern_opt {
    let glob = Glob::new(configuration_name_pattern)?.compile_matcher();

    cfs_session_vec.retain(|cfs_session| {
      cfs_session
        .configuration_name()
        .is_some_and(|configuration_name| glob.is_match(configuration_name))
    });
  };

  // Checks either target.groups contains hsm_group_name or ansible.limit is a subset of
  // hsm_group.members.ids
  cfs_session_vec.retain(|cfs_session| {
    cfs_session.get_target_hsm().is_some_and(|target_hsm_vec| {
      (keep_generic_sessions && is_session_image_generic(cfs_session))
        || target_hsm_vec.iter().any(|target_hsm| {
          hsm_group_name_available_vec
            .iter()
            .any(|hsm_group_name| target_hsm.contains(hsm_group_name))
        })
    }) || cfs_session
      .get_target_xname()
      .is_some_and(|target_xname_vec| {
        target_xname_vec
          .iter()
          .any(|target_xname| xname_available_vec.contains(&target_xname))
      })
  });

  if type_opt.is_some() {
    cfs_session_vec
      .retain(|cfs_session| cfs_session.get_target_def() == type_opt.cloned());
  }

  // Sort CFS sessions by start time order ASC
  cfs_session_vec.sort_by(|a, b| {
    a.get_start_time()
      .unwrap_or_default()
      .cmp(&b.get_start_time().unwrap_or_default())
  });

  if let Some(limit_number) = limit_number_opt {
    // Limiting the number of results to return to client
    *cfs_session_vec = cfs_session_vec
      [cfs_session_vec.len().saturating_sub(*limit_number as usize)..]
      .to_vec();
  }

  Ok(())
}

/// Filter CFS sessions to the ones related to a CFS configuration
pub fn filter_by_cofiguration(
  cfs_session_vec: &mut Vec<CfsSessionGetResponse>,
  cfs_configuration_name: &str,
) {
  log::info!(
    "Filter CFS sessions by configuration name '{}'",
    cfs_configuration_name
  );

  cfs_session_vec.retain(|cfs_session| {
    cfs_session.configuration_name().as_deref() == Some(cfs_configuration_name)
  });
}

/// Filter CFS sessions related to a list of HSM group names and a list of nodes and filter
/// all CFS sessions in the system using either the HSM group names or nodes as target.
/// NOTE: Please make sure the user has access to the HSM groups and nodes he is asking for before
/// calling this function
pub fn find_cfs_session_related_to_image_id(
  cfs_session_vec: &[CfsSessionGetResponse],
  image_id: &str,
) -> Option<CfsSessionGetResponse> {
  cfs_session_vec
    .iter()
    .find(|cfs_session| {
      cfs_session
        .first_result_id()
        .is_some_and(|result_id| result_id == image_id)
    })
    .cloned()
}

/// Returns a tuple like (image_id, cfs_configuration_name, target) from a list of CFS
/// sessions
pub fn get_image_id_cfs_configuration_target_tuple_vec(
  cfs_session_vec: &Vec<CfsSessionGetResponse>,
) -> Vec<(String, String, Vec<String>)> {
  let mut image_id_cfs_configuration_target_from_cfs_session: Vec<(
    String,
    String,
    Vec<String>,
  )> = Vec::new();

  cfs_session_vec.iter().for_each(|cfs_session| {
    let result_id: &str = cfs_session.first_result_id().unwrap_or_default();

    let target: Vec<String> = cfs_session
      .get_target_hsm()
      .or_else(|| cfs_session.get_target_xname())
      .unwrap_or_default();

    let cfs_configuration = cfs_session
      .configuration_name()
      .ok_or_else(|| {
        Error::SessionConfigurationNotDefined(cfs_session.name.clone())
      })
      .unwrap_or_default();

    image_id_cfs_configuration_target_from_cfs_session.push((
      result_id.to_string(),
      cfs_configuration.to_string(),
      target,
    ));
  });

  image_id_cfs_configuration_target_from_cfs_session
}

/// Returns a tuple like (image_id, cfs_configuration_name, target) from a list of CFS
/// sessions. Only returns values from CFS sessions with an artifact.result_id value
/// (meaning CFS sessions completed and successful of type image)
pub fn get_image_id_cfs_configuration_target_for_existing_images_tuple_vec(
  cfs_session_vec: &[CfsSessionGetResponse],
) -> Result<Vec<(String, String, Vec<String>)>, Error> {
  let mut image_id_cfs_configuration_target_from_cfs_session: Vec<(
    String,
    String,
    Vec<String>,
  )> = Vec::new();

  for cfs_session in cfs_session_vec {
    if let Some(result_id) = cfs_session.first_result_id() {
      let target: Vec<String> = cfs_session
        .get_target_hsm()
        .or_else(|| cfs_session.get_target_xname())
        .unwrap_or_default();

      let cfs_configuration =
        cfs_session.configuration_name().ok_or_else(|| {
          Error::SessionConfigurationNotDefined(cfs_session.name.clone())
        })?;

      image_id_cfs_configuration_target_from_cfs_session.push((
        result_id.to_string(),
        cfs_configuration.to_string(),
        target,
      ));
    } else {
      image_id_cfs_configuration_target_from_cfs_session.push((
        "".to_string(),
        "".to_string(),
        vec![],
      ));
    }
  }

  Ok(image_id_cfs_configuration_target_from_cfs_session)
}

/// Return a list of the images ids related with a list of CFS sessions. The result list if
/// filtered to CFS session completed and target def 'image' therefore the length of the
/// resulting list may be smaller than the list of CFS sessions
#[deprecated(note = "Use `images_id_from_cfs_session` instead")]
pub fn get_image_id_from_cfs_session_vec(
  cfs_session_vec: &[CfsSessionGetResponse],
) -> Vec<String> {
  let mut image_id_vec: Vec<String> = cfs_session_vec
    .iter()
    .flat_map(|cfs_session| cfs_session.results_id())
    .map(str::to_string)
    .collect();

  image_id_vec.sort();
  image_id_vec.dedup();

  image_id_vec
}

/// Return a list of the images ids related with a list of CFS sessions. The result list if
/// filtered to CFS session completed and target def 'image' therefore the length of the
/// resulting list may be smaller than the list of CFS sessions
pub fn images_id_from_cfs_session(
  cfs_session_vec: &[CfsSessionGetResponse],
) -> impl Iterator<Item = &str> {
  let mut image_id_vec: Vec<&str> = cfs_session_vec
    .iter()
    .flat_map(|cfs_session| cfs_session.results_id())
    .collect();

  image_id_vec.sort();
  image_id_vec.dedup();

  image_id_vec.into_iter()
}

/// Wait a CFS session to finish
// FIXME: This function prints CFS session logs to stdout which is not a good idea because the
// client may run outside this machine
pub async fn wait_cfs_session_to_finish(
  shasta_token: &str,
  shasta_base_url: &str,
  shasta_root_cert: &[u8],
  cfs_session_id: &str,
) -> Result<(), Error> {
  let mut i = 0;
  let max = 3000; // Max ammount of attempts to check if CFS session has ended
  loop {
    let cfs_session_vec = cfs::session::get_and_sort(
      shasta_token,
      shasta_base_url,
      shasta_root_cert,
      None,
      None,
      None,
      Some(&cfs_session_id.to_string()),
      None,
    )
    .await?;

    let cfs_session = if cfs_session_vec.is_empty() {
      return Err(Error::Message(format!(
        "ERROR - CFS session '{}' missing. Exit",
        cfs_session_id
      )));
    } else {
      cfs_session_vec
        .first()
        .ok_or_else(|| Error::SessionNotFound(cfs_session_id.to_string()))?
    };

    log::debug!("CFS session details:\n{:#?}", cfs_session);

    let cfs_session_status = cfs_session
      .status
      .as_ref()
      .and_then(|status| status.session.as_ref())
      .and_then(|session| session.status.as_deref())
      .ok_or_else(|| {
        Error::Message(format!(
          "ERROR - CFS session '{}' has no status. Exit",
          cfs_session_id
        ))
      })?;

    if cfs_session_status != "complete" && i < max {
      print!("\x1B[2K"); // Clear current line
      io::stdout().flush()?;
      println!(
        "Waiting CFS session '{}' with status '{}'. Checking again in 2 secs. Attempt {} of {}.",
        cfs_session_id, cfs_session_status, i, max
      );
      io::stdout().flush()?;

      tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;

      i += 1;
    } else {
      println!(
        "CFS session '{}' finished with status '{}'",
        cfs_session_id, cfs_session_status
      );
      break Ok(());
    }
  }
}

pub async fn get_list_xnames_related_to_session(
  group_available_vec: Vec<Group>,
  cfs_session: CfsSessionGetResponse,
) -> Result<Vec<String>, Error> {
  let target_group_xname_vec: Vec<String> = if let Some(target_hsm_vec) =
    cfs_session.get_target_hsm()
  {
    group_available_vec
      .into_iter()
      .filter(|group_available| target_hsm_vec.contains(&group_available.label))
      .flat_map(|group_available| group_available.get_members())
      .collect()
  } else {
    vec![]
  };

  let target_xname_vec =
    if let Some(target_xname) = cfs_session.get_target_xname() {
      target_xname
    } else {
      vec![]
    };

  Ok([target_xname_vec, target_group_xname_vec].concat())
}