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;
11use crate::server::common::app_context::InfraContext;
12pub use manta_shared::shared::params::kernel_parameters::GetKernelParametersParams;
13
14/// Fetch kernel boot parameters for the specified nodes.
15///
16/// Resolves target nodes from HSM group or node list, then
17/// fetches their BSS boot parameters.
18pub async fn get_kernel_parameters(
19  infra: &InfraContext<'_>,
20  token: &str,
21  params: &GetKernelParametersParams,
22) -> Result<Vec<BootParameters>, Error> {
23  let xname_vec = common::node_ops::resolve_target_nodes(
24    infra.backend,
25    token,
26    params.nodes.as_deref(),
27    params.hsm_group.as_deref(),
28    params.settings_hsm_group_name.as_deref(),
29  )
30  .await?;
31
32  let boot_parameter_vec =
33    infra.backend.get_bootparameters(token, &xname_vec).await?;
34
35  Ok(boot_parameter_vec)
36}
37
38/// Describes which kernel parameter mutation to apply.
39pub enum KernelParamOperation<'a> {
40  /// Add kernel parameters, optionally overwriting existing values.
41  Add {
42    /// Space-separated `key=value` pairs to add.
43    params: &'a str,
44    /// When true, replace existing parameters with the same key
45    /// instead of skipping them.
46    overwrite: bool,
47  },
48  /// Replace all kernel parameters with the given value.
49  Apply {
50    /// Space-separated `key=value` pairs that fully replace the
51    /// existing parameter set.
52    params: &'a str,
53  },
54  /// Remove the specified kernel parameters.
55  Delete {
56    /// Space-separated parameter names (or `key=value` pairs) to
57    /// remove.
58    params: &'a str,
59  },
60}
61
62impl<'a> KernelParamOperation<'a> {
63  /// Apply the mutation to a single `BootParameters` entry.
64  /// Returns `true` if the parameters were actually changed.
65  fn mutate(&self, boot_parameter: &mut BootParameters) -> bool {
66    match self {
67      Self::Add { params, overwrite } => {
68        boot_parameter.add_kernel_params(params, *overwrite)
69      }
70      Self::Apply { params } => boot_parameter.apply_kernel_params(params),
71      Self::Delete { params } => boot_parameter.delete_kernel_params(params),
72    }
73  }
74
75  /// Whether this operation should handle SBPS image projection.
76  fn handles_sbps_images(&self) -> bool {
77    match self {
78      Self::Add { .. } | Self::Apply { .. } => true,
79      Self::Delete { .. } => false,
80    }
81  }
82}
83
84/// Result of preparing kernel parameter mutations (before persistence).
85#[derive(serde::Serialize)]
86pub struct KernelParamsChangeset {
87  /// The mutated boot parameters ready to persist.
88  pub boot_params: Vec<BootParameters>,
89  /// Nodes that need rebooting.
90  pub xnames_to_reboot: Vec<String>,
91  /// Whether any changes were detected.
92  pub has_changes: bool,
93  /// SBPS images that need iSCSI projection (image_id -> Image).
94  /// The CLI layer should confirm with the user before persisting these.
95  pub sbps_candidates: Vec<(String, Image)>,
96}
97
98/// Fetch boot parameters, apply mutations, and return a changeset.
99///
100/// Does NOT persist anything — the caller decides whether to proceed.
101pub async fn prepare_kernel_params_changes(
102  infra: &InfraContext<'_>,
103  token: &str,
104  xname_vec: &[String],
105  operation: &KernelParamOperation<'_>,
106) -> Result<KernelParamsChangeset, Error> {
107  let mut boot_params: Vec<BootParameters> =
108    infra.backend.get_bootparameters(token, xname_vec).await?;
109
110  let mut has_changes = false;
111  let mut xnames_to_reboot: Vec<String> = Vec::new();
112  let mut seen_images: HashMap<String, bool> = HashMap::new();
113  let mut sbps_candidates: Vec<(String, Image)> = Vec::new();
114
115  for bp in &mut boot_params {
116    let changed = operation.mutate(bp);
117    if changed {
118      has_changes = true;
119      xnames_to_reboot.extend(bp.hosts.iter().cloned());
120    }
121
122    // Detect SBPS image candidates (add & apply only)
123    if operation.handles_sbps_images()
124      && let Some(image_id) = bp.try_get_boot_image_id()
125      && !seen_images.contains_key(&image_id)
126    {
127      seen_images.insert(image_id.clone(), true);
128
129      let image: Image = infra
130        .backend
131        .get_images(token, Some(&image_id))
132        .await?
133        .first()
134        .ok_or_else(|| {
135          Error::NotFound("No image found for the given image id".to_string())
136        })?
137        .clone();
138
139      if bp.is_root_kernel_param_iscsi_ready() {
140        sbps_candidates.push((image_id, image));
141      }
142    }
143  }
144
145  Ok(KernelParamsChangeset {
146    boot_params,
147    xnames_to_reboot,
148    has_changes,
149    sbps_candidates,
150  })
151}
152
153/// Persist the kernel parameter changes and optionally update SBPS images.
154pub async fn apply_kernel_params_changes(
155  infra: &InfraContext<'_>,
156  token: &str,
157  changeset: &KernelParamsChangeset,
158  images_to_project: &HashMap<String, Image>,
159) -> Result<(), Error> {
160  // Update boot parameters
161  for bp in &changeset.boot_params {
162    infra.backend.update_bootparameters(token, bp).await?;
163  }
164
165  // Update images projected through SBPS
166  for image in images_to_project.values() {
167    infra
168      .backend
169      .update_image(
170        token,
171        image
172          .id
173          .clone()
174          .ok_or_else(|| Error::MissingField("Image has no id".to_string()))?
175          .as_str(),
176        &image.clone().into(),
177      )
178      .await?;
179  }
180
181  Ok(())
182}
183
184/// Build the SBPS images-to-project map from a kernel params changeset.
185///
186/// Marks each candidate image as iSCSI-ready and returns the projection
187/// map. Returns an empty map when `project_sbps` is false.
188pub fn build_images_to_project(
189  changeset: &KernelParamsChangeset,
190  project_sbps: bool,
191) -> HashMap<String, Image> {
192  if !project_sbps {
193    return HashMap::new();
194  }
195  changeset
196    .sbps_candidates
197    .iter()
198    .map(|(id, img)| {
199      let mut img = img.clone();
200      img.set_boot_image_iscsi_ready();
201      (id.clone(), img)
202    })
203    .collect()
204}
205
206#[cfg(test)]
207mod tests {
208  use super::*;
209
210  fn add(params: &str) -> KernelParamOperation<'_> {
211    KernelParamOperation::Add {
212      params,
213      overwrite: false,
214    }
215  }
216  fn add_overwrite(params: &str) -> KernelParamOperation<'_> {
217    KernelParamOperation::Add {
218      params,
219      overwrite: true,
220    }
221  }
222  fn apply(params: &str) -> KernelParamOperation<'_> {
223    KernelParamOperation::Apply { params }
224  }
225  fn delete(params: &str) -> KernelParamOperation<'_> {
226    KernelParamOperation::Delete { params }
227  }
228
229  #[test]
230  fn overwrite_flag_preserved() {
231    match add_overwrite("x") {
232      KernelParamOperation::Add { overwrite, .. } => assert!(overwrite),
233      _ => panic!("wrong variant"),
234    }
235    match add("x") {
236      KernelParamOperation::Add { overwrite, .. } => assert!(!overwrite),
237      _ => panic!("wrong variant"),
238    }
239  }
240
241  #[test]
242  fn handles_sbps_images_only_for_add_and_apply() {
243    assert!(add("quiet").handles_sbps_images());
244    assert!(apply("quiet").handles_sbps_images());
245    assert!(!delete("quiet").handles_sbps_images());
246  }
247}