manta_server/service/
session.rs

1//! CFS session queries, creation, deletion, and console-readiness validation.
2
3use 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::server::common::authorization::get_groups_names_available;
15pub use manta_shared::shared::params::session::GetSessionParams;
16
17/// Fetch and filter CFS sessions from the backend.
18pub async fn get_sessions(
19  infra: &InfraContext<'_>,
20  token: &str,
21  params: &GetSessionParams,
22) -> Result<Vec<CfsSessionGetResponse>, Error> {
23  tracing::info!("Get CFS sessions");
24
25  infra
26    .backend
27    .get_and_filter_sessions(
28      token,
29      infra.shasta_base_url,
30      infra.shasta_root_cert,
31      params
32        .hsm_group
33        .as_ref()
34        .map(|v| vec![v.clone()])
35        .unwrap_or_default(),
36      params.xnames.iter().map(String::as_str).collect(),
37      params.min_age.as_ref(),
38      params.max_age.as_ref(),
39      params.session_type.as_ref(),
40      params.status.as_ref(),
41      params.name.as_ref(),
42      params.limit.as_ref(),
43      None,
44    )
45    .await
46}
47
48/// Data needed to delete/cancel a session.
49#[derive(serde::Serialize)]
50pub struct SessionDeletionContext {
51  /// The session to be deleted.
52  pub session: CfsSessionGetResponse,
53  /// IMS image IDs produced by this session (empty for non-image sessions).
54  pub image_ids: Vec<String>,
55  /// All HSM groups the token has access to (used for membership checks).
56  pub group_available_vec: Vec<Group>,
57  /// CFS component states (used to clear desired-config references).
58  pub cfs_component_vec: Vec<Component>,
59  /// BSS boot parameters (used to unset boot image refs pointing at session images).
60  pub bss_bootparameters_vec: Vec<BootParameters>,
61}
62
63/// Fetch session and related data, validate session exists.
64pub async fn prepare_session_deletion(
65  infra: &InfraContext<'_>,
66  token: &str,
67  session_name: &str,
68  settings_hsm_group_name_opt: Option<&str>,
69) -> Result<SessionDeletionContext, Error> {
70  let group_available_names = get_groups_names_available(
71    infra.backend,
72    token,
73    None,
74    settings_hsm_group_name_opt,
75  )
76  .await?;
77
78  tracing::info!("Fetching data from the backend...");
79  let start = std::time::Instant::now();
80
81  let (
82    group_available_vec,
83    cfs_session_vec,
84    cfs_component_vec,
85    bss_bootparameters_vec,
86  ) = tokio::try_join!(
87    infra.backend.get_group_available(token),
88    infra.backend.get_and_filter_sessions(
89      token,
90      infra.shasta_base_url,
91      infra.shasta_root_cert,
92      group_available_names,
93      Vec::new(),
94      None,
95      None,
96      None,
97      None,
98      None,
99      None,
100      None,
101    ),
102    infra.backend.get_cfs_components(
103      token,
104      infra.shasta_base_url,
105      infra.shasta_root_cert,
106      None,
107      None,
108      None,
109    ),
110    infra.backend.get_all_bootparameters(token),
111  )?;
112
113  tracing::info!(
114    "Time elapsed to fetch information from backend: {:?}",
115    start.elapsed()
116  );
117
118  let session = cfs_session_vec
119    .into_iter()
120    .find(|s| s.name == session_name)
121    .ok_or_else(|| Error::NotFound(format!("CFS session '{session_name}'")))?;
122
123  let image_ids = session.get_result_id_vec();
124
125  Ok(SessionDeletionContext {
126    session,
127    image_ids,
128    group_available_vec,
129    cfs_component_vec,
130    bss_bootparameters_vec,
131  })
132}
133
134/// Execute the session deletion.
135pub async fn execute_session_deletion(
136  infra: &InfraContext<'_>,
137  token: &str,
138  deletion_ctx: &SessionDeletionContext,
139  dry_run: bool,
140) -> Result<(), Error> {
141  infra
142    .backend
143    .delete_and_cancel_session(
144      token,
145      infra.shasta_base_url,
146      infra.shasta_root_cert,
147      &deletion_ctx.group_available_vec,
148      &deletion_ctx.session,
149      &deletion_ctx.cfs_component_vec,
150      &deletion_ctx.bss_bootparameters_vec,
151      dry_run,
152    )
153    .await
154}
155
156/// Resolve ansible-limit hosts to xnames and create a CFS session.
157///
158/// Returns `(cfs_configuration_name, cfs_session_name)`.
159#[allow(clippy::too_many_arguments)]
160pub async fn create_cfs_session(
161  infra: &InfraContext<'_>,
162  token: &str,
163  gitea_token: &str,
164  cfs_conf_sess_name: Option<&str>,
165  playbook_yaml_file_name_opt: Option<&str>,
166  hsm_group_opt: Option<&str>,
167  repo_name_vec: &[&str],
168  repo_last_commit_id_vec: &[&str],
169  ansible_limit_opt: Option<&str>,
170  ansible_verbosity: Option<&str>,
171  ansible_passthrough: Option<&str>,
172) -> Result<(String, String), Error> {
173  let backend = infra.backend;
174
175  let ansible_limit = if let Some(ansible_limit) = ansible_limit_opt {
176    let xname_vec = crate::server::common::node_ops::resolve_hosts_expression(
177      backend,
178      token,
179      ansible_limit,
180      false,
181    )
182    .await?;
183    Some(xname_vec.join(","))
184  } else {
185    None
186  };
187
188  backend
189    .apply_session(
190      gitea_token,
191      infra.gitea_base_url,
192      token,
193      infra.shasta_base_url,
194      infra.shasta_root_cert,
195      cfs_conf_sess_name,
196      playbook_yaml_file_name_opt,
197      hsm_group_opt,
198      repo_name_vec,
199      repo_last_commit_id_vec,
200      ansible_limit.as_deref(),
201      ansible_verbosity,
202      ansible_passthrough,
203    )
204    .await
205}
206
207/// Validate that a CFS session is suitable for attaching a console.
208///
209/// Returns `NotFound` if the session doesn't exist, `BadRequest` if the
210/// session is not image-type or has missing internal state, and `Conflict`
211/// if it is not running.
212pub async fn validate_console_session(
213  infra: &InfraContext<'_>,
214  token: &str,
215  name: &str,
216) -> Result<(), Error> {
217  let sessions = infra
218    .backend
219    .get_and_filter_sessions(
220      token,
221      infra.shasta_base_url,
222      infra.shasta_root_cert,
223      Vec::new(),
224      Vec::new(),
225      None,
226      None,
227      None,
228      None,
229      Some(&name.to_string()),
230      None,
231      None,
232    )
233    .await?;
234
235  let session = sessions
236    .first()
237    .ok_or_else(|| Error::NotFound(format!("CFS session '{name}'")))?;
238
239  let target_def = session
240    .target
241    .as_ref()
242    .and_then(|t| t.definition.as_ref())
243    .ok_or_else(|| {
244      Error::BadRequest(format!(
245        "CFS session '{name}' has no target definition"
246      ))
247    })?;
248
249  if target_def != "image" {
250    return Err(Error::BadRequest(format!(
251      "CFS session '{name}' is not an image-type session (got '{target_def}')"
252    )));
253  }
254
255  let status = session
256    .status
257    .as_ref()
258    .and_then(|s| s.session.as_ref())
259    .and_then(|s| s.status.as_ref())
260    .ok_or_else(|| {
261      Error::BadRequest(format!("CFS session '{name}' has no status"))
262    })?;
263
264  if status != "running" {
265    return Err(Error::Conflict(format!(
266      "CFS session '{name}' is not running (status: '{status}')"
267    )));
268  }
269
270  Ok(())
271}