use manta_backend_dispatcher::error::Error;
use manta_backend_dispatcher::interfaces::apply_session::ApplySessionTrait;
use manta_backend_dispatcher::interfaces::bss::BootParametersTrait;
use manta_backend_dispatcher::interfaces::cfs::CfsTrait;
use manta_backend_dispatcher::interfaces::hsm::group::GroupTrait;
use manta_backend_dispatcher::types::Group;
use manta_backend_dispatcher::types::bss::BootParameters;
use manta_backend_dispatcher::types::cfs::component::Component;
use manta_backend_dispatcher::types::cfs::session::CfsSessionGetResponse;
use crate::server::common::app_context::InfraContext;
use crate::service::authorization::{
validate_user_group_members_access, validate_user_group_vec_access,
};
use crate::service::node_ops;
pub use manta_shared::types::api::session::GetSessionParams;
pub async fn get_sessions(
infra: &InfraContext<'_>,
token: &str,
params: &GetSessionParams,
) -> Result<Vec<CfsSessionGetResponse>, Error> {
tracing::info!("Get CFS sessions");
let target_group_vec: Vec<String> = if !params.xnames.is_empty() {
Vec::new()
} else if let Some(group) = ¶ms.group {
vec![group.clone()]
} else {
infra
.backend
.get_group_available(token)
.await?
.iter()
.map(|group| group.label.clone())
.collect()
};
validate_user_group_vec_access(infra, token, &target_group_vec).await?;
validate_user_group_members_access(infra, token, ¶ms.xnames).await?;
infra
.backend
.get_and_filter_sessions(
token,
target_group_vec,
params.xnames.iter().map(|xname| xname.as_ref()).collect(),
params.min_age.as_ref(),
params.max_age.as_ref(),
params.session_type.as_ref(),
params.status.as_ref(),
params.name.as_ref(),
params.limit.as_ref(),
None,
)
.await
}
#[derive(serde::Serialize)]
pub struct SessionDeletionContext {
pub session: CfsSessionGetResponse,
pub image_ids: Vec<String>,
pub group_available_vec: Vec<Group>,
pub cfs_component_vec: Vec<Component>,
pub bss_bootparameters_vec: Vec<BootParameters>,
}
pub async fn prepare_session_deletion(
infra: &InfraContext<'_>,
token: &str,
session_name: &str,
settings_group_name_opt: Option<&str>,
) -> Result<SessionDeletionContext, Error> {
let (group_available_vec, target_group_vec) =
crate::service::group::resolve_target_and_available_groups(
infra,
token,
settings_group_name_opt,
)
.await?;
tracing::info!("Fetching data from the backend...");
let start = std::time::Instant::now();
let (cfs_session_vec, cfs_component_vec, bss_bootparameters_vec) = tokio::try_join!(
infra.backend.get_and_filter_sessions(
token,
target_group_vec,
Vec::new(),
None,
None,
None,
None,
None,
None,
None,
),
infra.backend.get_cfs_components(token, None, None, None),
infra.backend.get_all_bootparameters(token),
)?;
tracing::info!(
"Time elapsed to fetch information from backend: {:?}",
start.elapsed()
);
let session = cfs_session_vec
.into_iter()
.find(|s| s.name == session_name)
.ok_or_else(|| Error::NotFound(format!("CFS session '{session_name}'")))?;
let image_ids = session.get_result_id_vec();
Ok(SessionDeletionContext {
session,
image_ids,
group_available_vec,
cfs_component_vec,
bss_bootparameters_vec,
})
}
pub async fn execute_session_deletion(
infra: &InfraContext<'_>,
token: &str,
deletion_ctx: &SessionDeletionContext,
dry_run: bool,
) -> Result<(), Error> {
infra
.backend
.delete_and_cancel_session(
token,
&deletion_ctx.group_available_vec,
&deletion_ctx.session,
&deletion_ctx.cfs_component_vec,
&deletion_ctx.bss_bootparameters_vec,
dry_run,
)
.await
}
pub struct CreateCfsSessionParams<'a> {
pub cfs_conf_sess_name: Option<&'a str>,
pub playbook_yaml_file_name: Option<&'a str>,
pub group: Option<&'a str>,
pub repo_names: &'a [&'a str],
pub repo_last_commit_ids: &'a [&'a str],
pub ansible_limit: Option<&'a str>,
pub ansible_verbosity: Option<&'a str>,
pub ansible_passthrough: Option<&'a str>,
}
pub async fn create_cfs_session(
infra: &InfraContext<'_>,
token: &str,
gitea_token: &str,
params: CreateCfsSessionParams<'_>,
) -> Result<(String, String), Error> {
let ansible_limit = if let Some(ansible_limit) = params.ansible_limit {
let xname_vec = node_ops::from_user_hosts_expression_to_xname_vec(
infra,
token,
ansible_limit,
false,
)
.await?;
Some(xname_vec.join(","))
} else {
None
};
infra
.backend
.apply_session(
gitea_token,
infra.gitea_base_url,
token,
params.cfs_conf_sess_name,
params.playbook_yaml_file_name,
params.group,
params.repo_names,
params.repo_last_commit_ids,
ansible_limit.as_deref(),
params.ansible_verbosity,
params.ansible_passthrough,
)
.await
}
pub async fn validate_session_access(
infra: &InfraContext<'_>,
token: &str,
session_name: &str,
) -> Result<CfsSessionGetResponse, Error> {
let sessions = infra
.backend
.get_and_filter_sessions(
token,
Vec::new(),
Vec::new(),
None,
None,
None,
None,
Some(&session_name.to_string()),
None,
None,
)
.await?;
let session = sessions
.into_iter()
.next()
.ok_or_else(|| Error::NotFound(format!("CFS session '{session_name}'")))?;
let target_groups = session.get_target_hsm().unwrap_or_default();
if !target_groups.is_empty() {
let accessible = infra.backend.get_group_name_available(token).await?;
if let Some(unauthorized) =
target_groups.iter().find(|g| !accessible.contains(g))
{
return Err(Error::BadRequest(format!(
"Can't access CFS session '{session_name}': it targets HSM \
group '{unauthorized}' which is not in your accessible set"
)));
}
}
Ok(session)
}
pub fn require_result_image(
session: &CfsSessionGetResponse,
) -> Result<(), Error> {
if session.get_first_result_id().is_none() {
return Err(Error::BadRequest(format!(
"CFS session '{}' produced no image (no result_id); refusing to stamp",
session.name
)));
}
Ok(())
}
pub async fn validate_console_session(
infra: &InfraContext<'_>,
token: &str,
name: &str,
) -> Result<(), Error> {
let sessions = infra
.backend
.get_and_filter_sessions(
token,
Vec::new(),
Vec::new(),
None,
None,
None,
None,
Some(&name.to_string()),
None,
None,
)
.await?;
let session = sessions
.first()
.ok_or_else(|| Error::NotFound(format!("CFS session '{name}'")))?;
let target_def = session
.target
.as_ref()
.and_then(|t| t.definition.as_ref())
.ok_or_else(|| {
Error::BadRequest(format!(
"CFS session '{name}' has no target definition"
))
})?;
if target_def != "image" {
return Err(Error::BadRequest(format!(
"CFS session '{name}' is not an image-type session (got '{target_def}')"
)));
}
let status = session
.status
.as_ref()
.and_then(|s| s.session.as_ref())
.and_then(|s| s.status.as_ref())
.ok_or_else(|| {
Error::BadRequest(format!("CFS session '{name}' has no status"))
})?;
if status != "running" {
return Err(Error::Conflict(format!(
"CFS session '{name}' is not running (status: '{status}')"
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{Error, require_result_image};
use manta_backend_dispatcher::types::cfs::session::{
Artifact, CfsSessionGetResponse, Status,
};
fn session_with_result_id(
name: &str,
result_id: Option<&str>,
) -> CfsSessionGetResponse {
CfsSessionGetResponse {
name: name.to_string(),
configuration: None,
ansible: None,
target: None,
status: Some(Status {
artifacts: Some(vec![Artifact {
image_id: None,
result_id: result_id.map(str::to_string),
r#type: None,
}]),
session: None,
}),
tags: None,
debug_on_failure: false,
logs: None,
}
}
#[test]
fn require_result_image_accepts_session_with_result_id() {
let session = session_with_result_id("sat-img-v1", Some("ims-image-abc"));
assert!(require_result_image(&session).is_ok());
}
#[test]
fn require_result_image_rejects_session_without_result_id() {
let session = session_with_result_id("sat-img-v1", None);
let err = require_result_image(&session).unwrap_err();
assert!(
matches!(err, Error::BadRequest(_)),
"expected BadRequest, got {err:?}"
);
assert!(err.to_string().contains("sat-img-v1"));
assert!(err.to_string().contains("no result_id"));
}
#[test]
fn require_result_image_rejects_session_with_no_artifacts() {
let session = CfsSessionGetResponse {
name: "sat-img-v1".to_string(),
configuration: None,
ansible: None,
target: None,
status: None,
tags: None,
debug_on_failure: false,
logs: None,
};
let err = require_result_image(&session).unwrap_err();
assert!(matches!(err, Error::BadRequest(_)));
}
}