manta_server/service/
kernel_parameters.rs

1//! Kernel boot parameter mutations (add, apply, delete) with SBPS iSCSI image projection.
2
3use manta_backend_dispatcher::error::Error;
4use manta_backend_dispatcher::interfaces::bss::BootParametersTrait;
5use manta_backend_dispatcher::interfaces::ims::ImsTrait;
6use manta_backend_dispatcher::types::bss::BootParameters;
7use manta_backend_dispatcher::types::ims::Image;
8use std::collections::HashMap;
9
10use crate::server::common::app_context::InfraContext;
11use crate::service::authorization::validate_user_group_members_access;
12use crate::service::node_ops;
13pub use manta_shared::types::api::kernel_parameters::GetKernelParametersParams;
14
15/// Fetch BSS kernel parameters for the targets described by `params`.
16///
17/// Targets are resolved through [`node_ops::resolve_target_nodes`]
18/// (host expression → `group_name` → `settings_group_name` fallback
19/// from `cli.toml`). The caller's access to every resolved xname is
20/// validated before the BSS query runs.
21pub async fn get_kernel_parameters(
22  infra: &InfraContext<'_>,
23  token: &str,
24  params: &GetKernelParametersParams,
25) -> Result<Vec<BootParameters>, Error> {
26  let xname_vec = node_ops::resolve_target_nodes(
27    infra,
28    token,
29    params.nodes.as_deref(),
30    params.group_name.as_deref(),
31    params.settings_group_name.as_deref(),
32  )
33  .await?;
34
35  validate_user_group_members_access(infra, token, &xname_vec).await?;
36
37  let boot_parameter_vec =
38    infra.backend.get_bootparameters(token, &xname_vec).await?;
39
40  Ok(boot_parameter_vec)
41}
42
43/// Describes which kernel parameter mutation to apply.
44pub(crate) enum KernelParamOperation<'a> {
45  /// Add kernel parameters, optionally overwriting existing values.
46  Add {
47    /// Space-separated `key=value` pairs to add.
48    params: &'a str,
49    /// When true, replace existing parameters with the same key
50    /// instead of skipping them.
51    overwrite: bool,
52  },
53  /// Replace all kernel parameters with the given value.
54  Apply {
55    /// Space-separated `key=value` pairs that fully replace the
56    /// existing parameter set.
57    params: &'a str,
58  },
59  /// Remove the specified kernel parameters.
60  Delete {
61    /// Space-separated parameter names (or `key=value` pairs) to
62    /// remove.
63    params: &'a str,
64  },
65}
66
67impl<'a> KernelParamOperation<'a> {
68  /// Apply the mutation to a single `BootParameters` entry.
69  /// Returns `true` if the parameters were actually changed.
70  fn mutate(&self, boot_parameter: &mut BootParameters) -> bool {
71    match self {
72      Self::Add { params, overwrite } => {
73        boot_parameter.add_kernel_params(params, *overwrite)
74      }
75      Self::Apply { params } => boot_parameter.apply_kernel_params(params),
76      Self::Delete { params } => boot_parameter.delete_kernel_params(params),
77    }
78  }
79
80  /// Whether this operation should handle SBPS image projection.
81  fn handles_sbps_images(&self) -> bool {
82    match self {
83      Self::Add { .. } | Self::Apply { .. } => true,
84      Self::Delete { .. } => false,
85    }
86  }
87}
88
89/// Result of preparing kernel parameter mutations (before persistence).
90#[derive(serde::Serialize)]
91pub struct KernelParamsChangeset {
92  /// The mutated boot parameters ready to persist.
93  pub boot_params: Vec<BootParameters>,
94  /// Nodes that need rebooting.
95  pub xnames_to_reboot: Vec<String>,
96  /// Whether any changes were detected.
97  pub has_changes: bool,
98  /// SBPS images that need iSCSI projection (image_id -> Image).
99  /// The CLI layer should confirm with the user before persisting these.
100  pub sbps_candidates: Vec<(String, Image)>,
101}
102
103/// Compute the kernel-parameter mutation as a
104/// [`KernelParamsChangeset`] without writing anything.
105///
106/// Pulls the current BSS records for `xname_vec`, applies `operation`
107/// to each in memory, and tracks which xnames actually changed so the
108/// caller can target the reboot list precisely. For `Add`/`Apply`,
109/// each unique boot-image referenced by a changed record is
110/// inspected once: if its root kernel-parameters look iSCSI-ready it
111/// is appended to `sbps_candidates` so the caller can decide whether
112/// to project it through SBPS.
113pub(crate) async fn prepare_kernel_params_changes(
114  infra: &InfraContext<'_>,
115  token: &str,
116  xname_vec: &[String],
117  operation: &KernelParamOperation<'_>,
118) -> Result<KernelParamsChangeset, Error> {
119  let mut boot_params: Vec<BootParameters> =
120    infra.backend.get_bootparameters(token, xname_vec).await?;
121
122  let mut has_changes = false;
123  let mut xnames_to_reboot: Vec<String> = Vec::new();
124
125  // First pass: apply the in-memory mutation and gather, in the
126  // order they first appear, the unique image ids referenced by
127  // iSCSI-ready boot parameters. The image fetches happen in the
128  // second pass below — the previous code fetched serially inside
129  // the loop (N HTTPS round-trips for N distinct boot images on a
130  // cluster-scale write).
131  let handles_sbps = operation.handles_sbps_images();
132  let mut sbps_image_ids: Vec<String> = Vec::new();
133  let mut seen_image_ids: std::collections::HashSet<String> =
134    std::collections::HashSet::new();
135
136  for bp in &mut boot_params {
137    let changed = operation.mutate(bp);
138    if changed {
139      has_changes = true;
140      xnames_to_reboot.extend(bp.hosts.iter().cloned());
141    }
142
143    if handles_sbps
144      && bp.is_root_kernel_param_iscsi_ready()
145      && let Some(image_id) = bp.try_get_boot_image_id()
146      && seen_image_ids.insert(image_id.clone())
147    {
148      sbps_image_ids.push(image_id);
149    }
150  }
151
152  // Second pass: resolve each unique image id in parallel.
153  let sbps_candidates: Vec<(String, Image)> = if sbps_image_ids.is_empty() {
154    Vec::new()
155  } else {
156    futures::future::try_join_all(sbps_image_ids.into_iter().map(|id| async {
157      let image = infra
158        .backend
159        .get_images(token, Some(id.as_str()))
160        .await?
161        .first()
162        .ok_or_else(|| {
163          Error::NotFound(format!("No image found for image id '{id}'"))
164        })?
165        .clone();
166      Ok::<_, Error>((id, image))
167    }))
168    .await?
169  };
170
171  Ok(KernelParamsChangeset {
172    boot_params,
173    xnames_to_reboot,
174    has_changes,
175    sbps_candidates,
176  })
177}
178
179/// Write a previously prepared [`KernelParamsChangeset`] back to BSS,
180/// and patch any SBPS images supplied in `images_to_project`.
181///
182/// Access to every reboot-target xname is re-validated before the
183/// first backend write. `images_to_project` is normally built by
184/// [`build_images_to_project`]; pass an empty map (as the delete path
185/// does) to skip SBPS projection entirely.
186pub async fn apply_kernel_params_changes(
187  infra: &InfraContext<'_>,
188  token: &str,
189  changeset: &KernelParamsChangeset,
190  images_to_project: &HashMap<String, Image>,
191) -> Result<(), Error> {
192  validate_user_group_members_access(infra, token, &changeset.xnames_to_reboot)
193    .await?;
194
195  // Update boot parameters
196  for bp in &changeset.boot_params {
197    infra.backend.update_bootparameters(token, bp).await?;
198  }
199
200  // Update images projected through SBPS
201  for image in images_to_project.values() {
202    infra
203      .backend
204      .update_image(
205        token,
206        image
207          .id
208          .clone()
209          .ok_or_else(|| Error::MissingField("Image has no id".to_string()))?
210          .as_str(),
211        &image.clone().into(),
212      )
213      .await?;
214  }
215
216  Ok(())
217}
218
219/// Build the SBPS images-to-project map from a kernel params changeset.
220///
221/// Marks each candidate image as iSCSI-ready and returns the projection
222/// map. Returns an empty map when `project_sbps` is false.
223pub fn build_images_to_project(
224  changeset: &KernelParamsChangeset,
225  project_sbps: bool,
226) -> HashMap<String, Image> {
227  if !project_sbps {
228    return HashMap::new();
229  }
230  changeset
231    .sbps_candidates
232    .iter()
233    .map(|(id, img)| {
234      let mut img = img.clone();
235      img.set_boot_image_iscsi_ready();
236      (id.clone(), img)
237    })
238    .collect()
239}
240
241#[cfg(test)]
242mod tests {
243  use super::*;
244
245  fn add(params: &str) -> KernelParamOperation<'_> {
246    KernelParamOperation::Add {
247      params,
248      overwrite: false,
249    }
250  }
251  fn add_overwrite(params: &str) -> KernelParamOperation<'_> {
252    KernelParamOperation::Add {
253      params,
254      overwrite: true,
255    }
256  }
257  fn apply(params: &str) -> KernelParamOperation<'_> {
258    KernelParamOperation::Apply { params }
259  }
260  fn delete(params: &str) -> KernelParamOperation<'_> {
261    KernelParamOperation::Delete { params }
262  }
263
264  #[test]
265  fn overwrite_flag_preserved() {
266    match add_overwrite("x") {
267      KernelParamOperation::Add { overwrite, .. } => assert!(overwrite),
268      _ => panic!("wrong variant"),
269    }
270    match add("x") {
271      KernelParamOperation::Add { overwrite, .. } => assert!(!overwrite),
272      _ => panic!("wrong variant"),
273    }
274  }
275
276  #[test]
277  fn handles_sbps_images_only_for_add_and_apply() {
278    assert!(add("quiet").handles_sbps_images());
279    assert!(apply("quiet").handles_sbps_images());
280    assert!(!delete("quiet").handles_sbps_images());
281  }
282}