1use manta_backend_dispatcher::error::Error;
4use manta_backend_dispatcher::interfaces::apply_session::ApplySessionTrait;
5use manta_backend_dispatcher::interfaces::bss::BootParametersTrait;
6use manta_backend_dispatcher::interfaces::cfs::CfsTrait;
7use manta_backend_dispatcher::interfaces::hsm::group::GroupTrait;
8use manta_backend_dispatcher::types::Group;
9use manta_backend_dispatcher::types::bss::BootParameters;
10use manta_backend_dispatcher::types::cfs::component::Component;
11use manta_backend_dispatcher::types::cfs::session::CfsSessionGetResponse;
12
13use crate::server::common::app_context::InfraContext;
14use crate::service::authorization::{
15 validate_user_group_members_access, validate_user_group_vec_access,
16};
17use crate::service::node_ops;
18pub use manta_shared::types::api::session::GetSessionParams;
19
20pub async fn get_sessions(
31 infra: &InfraContext<'_>,
32 token: &str,
33 params: &GetSessionParams,
34) -> Result<Vec<CfsSessionGetResponse>, Error> {
35 tracing::info!("Get CFS sessions");
36
37 let target_group_vec: Vec<String> = if !params.xnames.is_empty() {
42 Vec::new()
43 } else if let Some(group) = ¶ms.group {
44 vec![group.clone()]
45 } else {
46 infra
47 .backend
48 .get_group_available(token)
49 .await?
50 .iter()
51 .map(|group| group.label.clone())
52 .collect()
53 };
54
55 validate_user_group_vec_access(infra, token, &target_group_vec).await?;
56 validate_user_group_members_access(infra, token, ¶ms.xnames).await?;
57
58 infra
59 .backend
60 .get_and_filter_sessions(
61 token,
62 target_group_vec,
63 params.xnames.iter().map(|xname| xname.as_ref()).collect(),
64 params.min_age.as_ref(),
65 params.max_age.as_ref(),
66 params.session_type.as_ref(),
67 params.status.as_ref(),
68 params.name.as_ref(),
69 params.limit.as_ref(),
70 None,
71 )
72 .await
73}
74
75#[derive(serde::Serialize)]
77pub struct SessionDeletionContext {
78 pub session: CfsSessionGetResponse,
80 pub image_ids: Vec<String>,
82 pub group_available_vec: Vec<Group>,
84 pub cfs_component_vec: Vec<Component>,
86 pub bss_bootparameters_vec: Vec<BootParameters>,
88}
89
90pub async fn prepare_session_deletion(
101 infra: &InfraContext<'_>,
102 token: &str,
103 session_name: &str,
104 settings_group_name_opt: Option<&str>,
105) -> Result<SessionDeletionContext, Error> {
106 let (group_available_vec, target_group_vec) =
109 crate::service::group::resolve_target_and_available_groups(
110 infra,
111 token,
112 settings_group_name_opt,
113 )
114 .await?;
115
116 tracing::info!("Fetching data from the backend...");
117 let start = std::time::Instant::now();
118
119 let (cfs_session_vec, cfs_component_vec, bss_bootparameters_vec) = tokio::try_join!(
120 infra.backend.get_and_filter_sessions(
121 token,
122 target_group_vec,
123 Vec::new(),
124 None,
125 None,
126 None,
127 None,
128 None,
129 None,
130 None,
131 ),
132 infra.backend.get_cfs_components(token, None, None, None),
133 infra.backend.get_all_bootparameters(token),
134 )?;
135
136 tracing::info!(
137 "Time elapsed to fetch information from backend: {:?}",
138 start.elapsed()
139 );
140
141 let session = cfs_session_vec
142 .into_iter()
143 .find(|s| s.name == session_name)
144 .ok_or_else(|| Error::NotFound(format!("CFS session '{session_name}'")))?;
145
146 let image_ids = session.get_result_id_vec();
147
148 Ok(SessionDeletionContext {
149 session,
150 image_ids,
151 group_available_vec,
152 cfs_component_vec,
153 bss_bootparameters_vec,
154 })
155}
156
157pub async fn execute_session_deletion(
166 infra: &InfraContext<'_>,
167 token: &str,
168 deletion_ctx: &SessionDeletionContext,
169 dry_run: bool,
170) -> Result<(), Error> {
171 infra
172 .backend
173 .delete_and_cancel_session(
174 token,
175 &deletion_ctx.group_available_vec,
176 &deletion_ctx.session,
177 &deletion_ctx.cfs_component_vec,
178 &deletion_ctx.bss_bootparameters_vec,
179 dry_run,
180 )
181 .await
182}
183
184pub struct CreateCfsSessionParams<'a> {
188 pub cfs_conf_sess_name: Option<&'a str>,
191 pub playbook_yaml_file_name: Option<&'a str>,
193 pub group: Option<&'a str>,
195 pub repo_names: &'a [&'a str],
197 pub repo_last_commit_ids: &'a [&'a str],
199 pub ansible_limit: Option<&'a str>,
202 pub ansible_verbosity: Option<&'a str>,
204 pub ansible_passthrough: Option<&'a str>,
206}
207
208pub async fn create_cfs_session(
219 infra: &InfraContext<'_>,
220 token: &str,
221 gitea_token: &str,
222 params: CreateCfsSessionParams<'_>,
223) -> Result<(String, String), Error> {
224 let ansible_limit = if let Some(ansible_limit) = params.ansible_limit {
225 let xname_vec = node_ops::from_user_hosts_expression_to_xname_vec(
226 infra,
227 token,
228 ansible_limit,
229 false,
230 )
231 .await?;
232 Some(xname_vec.join(","))
233 } else {
234 None
235 };
236
237 infra
238 .backend
239 .apply_session(
240 gitea_token,
241 infra.gitea_base_url,
242 token,
243 params.cfs_conf_sess_name,
244 params.playbook_yaml_file_name,
245 params.group,
246 params.repo_names,
247 params.repo_last_commit_ids,
248 ansible_limit.as_deref(),
249 params.ansible_verbosity,
250 params.ansible_passthrough,
251 )
252 .await
253}
254
255pub async fn validate_session_access(
270 infra: &InfraContext<'_>,
271 token: &str,
272 session_name: &str,
273) -> Result<CfsSessionGetResponse, Error> {
274 let sessions = infra
275 .backend
276 .get_and_filter_sessions(
277 token,
278 Vec::new(),
279 Vec::new(),
280 None,
281 None,
282 None,
283 None,
284 Some(&session_name.to_string()),
285 None,
286 None,
287 )
288 .await?;
289
290 let session = sessions
291 .into_iter()
292 .next()
293 .ok_or_else(|| Error::NotFound(format!("CFS session '{session_name}'")))?;
294
295 let target_groups = session.get_target_hsm().unwrap_or_default();
296 if !target_groups.is_empty() {
297 let accessible = infra.backend.get_group_name_available(token).await?;
298 if let Some(unauthorized) =
299 target_groups.iter().find(|g| !accessible.contains(g))
300 {
301 return Err(Error::BadRequest(format!(
302 "Can't access CFS session '{session_name}': it targets HSM \
303 group '{unauthorized}' which is not in your accessible set"
304 )));
305 }
306 }
307
308 Ok(session)
309}
310
311pub fn require_result_image(
318 session: &CfsSessionGetResponse,
319) -> Result<(), Error> {
320 if session.get_first_result_id().is_none() {
321 return Err(Error::BadRequest(format!(
322 "CFS session '{}' produced no image (no result_id); refusing to stamp",
323 session.name
324 )));
325 }
326 Ok(())
327}
328
329pub async fn validate_console_session(
335 infra: &InfraContext<'_>,
336 token: &str,
337 name: &str,
338) -> Result<(), Error> {
339 let sessions = infra
340 .backend
341 .get_and_filter_sessions(
342 token,
343 Vec::new(),
344 Vec::new(),
345 None,
346 None,
347 None,
348 None,
349 Some(&name.to_string()),
350 None,
351 None,
352 )
353 .await?;
354
355 let session = sessions
356 .first()
357 .ok_or_else(|| Error::NotFound(format!("CFS session '{name}'")))?;
358
359 let target_def = session
360 .target
361 .as_ref()
362 .and_then(|t| t.definition.as_ref())
363 .ok_or_else(|| {
364 Error::BadRequest(format!(
365 "CFS session '{name}' has no target definition"
366 ))
367 })?;
368
369 if target_def != "image" {
370 return Err(Error::BadRequest(format!(
371 "CFS session '{name}' is not an image-type session (got '{target_def}')"
372 )));
373 }
374
375 let status = session
376 .status
377 .as_ref()
378 .and_then(|s| s.session.as_ref())
379 .and_then(|s| s.status.as_ref())
380 .ok_or_else(|| {
381 Error::BadRequest(format!("CFS session '{name}' has no status"))
382 })?;
383
384 if status != "running" {
385 return Err(Error::Conflict(format!(
386 "CFS session '{name}' is not running (status: '{status}')"
387 )));
388 }
389
390 Ok(())
391}
392
393#[cfg(test)]
394mod tests {
395 use super::{Error, require_result_image};
401 use manta_backend_dispatcher::types::cfs::session::{
402 Artifact, CfsSessionGetResponse, Status,
403 };
404
405 fn session_with_result_id(
406 name: &str,
407 result_id: Option<&str>,
408 ) -> CfsSessionGetResponse {
409 CfsSessionGetResponse {
410 name: name.to_string(),
411 configuration: None,
412 ansible: None,
413 target: None,
414 status: Some(Status {
415 artifacts: Some(vec![Artifact {
416 image_id: None,
417 result_id: result_id.map(str::to_string),
418 r#type: None,
419 }]),
420 session: None,
421 }),
422 tags: None,
423 debug_on_failure: false,
424 logs: None,
425 }
426 }
427
428 #[test]
429 fn require_result_image_accepts_session_with_result_id() {
430 let session = session_with_result_id("sat-img-v1", Some("ims-image-abc"));
431 assert!(require_result_image(&session).is_ok());
432 }
433
434 #[test]
435 fn require_result_image_rejects_session_without_result_id() {
436 let session = session_with_result_id("sat-img-v1", None);
437 let err = require_result_image(&session).unwrap_err();
438 assert!(
439 matches!(err, Error::BadRequest(_)),
440 "expected BadRequest, got {err:?}"
441 );
442 assert!(err.to_string().contains("sat-img-v1"));
443 assert!(err.to_string().contains("no result_id"));
444 }
445
446 #[test]
447 fn require_result_image_rejects_session_with_no_artifacts() {
448 let session = CfsSessionGetResponse {
449 name: "sat-img-v1".to_string(),
450 configuration: None,
451 ansible: None,
452 target: None,
453 status: None,
454 tags: None,
455 debug_on_failure: false,
456 logs: None,
457 };
458 let err = require_result_image(&session).unwrap_err();
459 assert!(matches!(err, Error::BadRequest(_)));
460 }
461}