manta_server/service/
boot_parameters.rs

1//! BSS boot parameter queries, changeset preparation, and persistence.
2
3use manta_backend_dispatcher::{
4  error::Error,
5  interfaces::{bss::BootParametersTrait, cfs::CfsTrait, ims::ImsTrait},
6  types::{
7    bss::BootParameters,
8    ims::{Image, PatchImage},
9  },
10};
11use std::collections::HashMap;
12
13use crate::manta_backend_dispatcher::StaticBackendDispatcher;
14use crate::server::common;
15use crate::server::common::app_context::InfraContext;
16use crate::server::common::authorization::validate_target_hsm_members;
17use crate::server::common::ims_ops::get_image_vec_related_cfs_configuration_name;
18pub use manta_shared::shared::params::boot_parameters::{
19  GetBootParametersParams, UpdateBootParametersParams,
20};
21
22/// Fetch boot parameters for the specified nodes.
23pub async fn get_boot_parameters(
24  infra: &InfraContext<'_>,
25  token: &str,
26  params: &GetBootParametersParams,
27) -> Result<Vec<BootParameters>, Error> {
28  let xname_vec = common::node_ops::resolve_target_nodes(
29    infra.backend,
30    token,
31    params.nodes.as_deref(),
32    params.hsm_group.as_deref(),
33    params.settings_hsm_group_name.as_deref(),
34  )
35  .await?;
36
37  tracing::info!("Get boot parameters");
38
39  infra.backend.get_bootparameters(token, &xname_vec).await
40}
41
42/// Delete boot parameters for the specified hosts.
43pub async fn delete_boot_parameters(
44  infra: &InfraContext<'_>,
45  token: &str,
46  hosts: Vec<String>,
47) -> Result<(), Error> {
48  let boot_parameters = BootParameters {
49    hosts,
50    macs: None,
51    nids: None,
52    params: String::new(),
53    kernel: String::new(),
54    initrd: String::new(),
55    cloud_init: None,
56  };
57
58  infra
59    .backend
60    .delete_bootparameters(token, &boot_parameters)
61    .await
62    .map(|_| ())
63}
64
65/// Add (create) boot parameters for specified nodes.
66pub async fn add_boot_parameters(
67  infra: &InfraContext<'_>,
68  token: &str,
69  boot_parameters: &BootParameters,
70) -> Result<(), Error> {
71  infra
72    .backend
73    .add_bootparameters(token, boot_parameters)
74    .await
75}
76
77/// Update boot parameters for specified nodes.
78pub async fn update_boot_parameters(
79  infra: &InfraContext<'_>,
80  token: &str,
81  params: UpdateBootParametersParams,
82) -> Result<(), Error> {
83  validate_target_hsm_members(infra.backend, token, &params.hosts).await?;
84
85  let boot_parameters = BootParameters {
86    hosts: params.hosts,
87    macs: params.macs,
88    nids: params.nids,
89    params: params.params,
90    kernel: params.kernel,
91    initrd: params.initrd,
92    cloud_init: None,
93  };
94
95  tracing::debug!("new boot params: {:#?}", boot_parameters);
96
97  infra
98    .backend
99    .update_bootparameters(token, &boot_parameters)
100    .await
101}
102
103/// Result of preparing boot configuration changes.
104#[derive(serde::Serialize)]
105pub struct BootConfigChangeset {
106  /// Resolved target xnames.
107  pub xname_vec: Vec<String>,
108  /// Updated BSS boot parameter records, ready to persist.
109  pub boot_param_vec: Vec<BootParameters>,
110  /// IMS images referenced by the new boot config, keyed by image ID.
111  pub image_vec: HashMap<String, Image>,
112  /// Whether nodes need a reboot to apply the new parameters.
113  pub need_restart: bool,
114}
115
116/// Prepare boot configuration changes (no side effects).
117pub async fn prepare_boot_config(
118  infra: &InfraContext<'_>,
119  token: &str,
120  hosts_expression: &str,
121  new_boot_image_id_opt: Option<&str>,
122  new_boot_image_configuration_opt: Option<&str>,
123  new_kernel_parameters_opt: Option<&str>,
124) -> Result<BootConfigChangeset, Error> {
125  let backend = infra.backend;
126
127  let mut need_restart = false;
128
129  let xname_vec = common::node_ops::resolve_hosts_expression(
130    backend,
131    token,
132    hosts_expression,
133    false,
134  )
135  .await?;
136
137  let mut current_node_boot_param_vec: Vec<BootParameters> =
138    backend.get_bootparameters(token, &xname_vec).await?;
139
140  let new_boot_image_opt = get_new_boot_image(
141    backend,
142    token,
143    infra.shasta_base_url,
144    infra.shasta_root_cert,
145    new_boot_image_configuration_opt,
146    new_boot_image_id_opt,
147  )
148  .await?;
149
150  // IMPORTANT: ALWAYS SET KERNEL PARAMS BEFORE BOOT IMAGE
151  if let Some(new_kernel_parameters) = new_kernel_parameters_opt {
152    need_restart |= apply_kernel_params(
153      &mut current_node_boot_param_vec,
154      new_kernel_parameters,
155    )?;
156  }
157
158  let mut image_vec = collect_boot_images(
159    backend,
160    token,
161    &mut current_node_boot_param_vec,
162    new_boot_image_opt,
163    &mut need_restart,
164  )
165  .await?;
166
167  if current_node_boot_param_vec
168    .first()
169    .ok_or_else(|| Error::NotFound("No boot parameters found".to_string()))?
170    .is_root_kernel_param_iscsi_ready()
171  {
172    for image in image_vec.values_mut() {
173      image.set_boot_image_iscsi_ready();
174    }
175  }
176
177  Ok(BootConfigChangeset {
178    xname_vec,
179    boot_param_vec: current_node_boot_param_vec,
180    image_vec,
181    need_restart,
182  })
183}
184
185/// Persist boot configuration changes.
186pub async fn persist_boot_config(
187  infra: &InfraContext<'_>,
188  token: &str,
189  changeset: &BootConfigChangeset,
190  new_runtime_configuration_opt: Option<&str>,
191) -> Result<(), Error> {
192  tracing::info!("Persist changes");
193
194  for boot_parameter in &changeset.boot_param_vec {
195    tracing::debug!("Updating boot parameter:\n{:#?}", boot_parameter);
196    let component_patch_rep = infra
197      .backend
198      .update_bootparameters(token, boot_parameter)
199      .await;
200    tracing::debug!(
201      "Component boot parameters resp:\n{:#?}",
202      component_patch_rep
203    );
204  }
205
206  if let Some(new_runtime_configuration_name) = new_runtime_configuration_opt {
207    println!(
208      "Updating runtime configuration to '{new_runtime_configuration_name}'"
209    );
210
211    infra
212      .backend
213      .update_runtime_configuration(
214        token,
215        infra.shasta_base_url,
216        infra.shasta_root_cert,
217        &changeset.xname_vec,
218        new_runtime_configuration_name,
219        true,
220      )
221      .await?;
222
223    for image in changeset.image_vec.values() {
224      let image_id = image.id.clone().ok_or_else(|| {
225        Error::MissingField("Image id is missing".to_string())
226      })?;
227      let patch_image: PatchImage = image.clone().into();
228      infra
229        .backend
230        .update_image(token, &image_id, &patch_image)
231        .await?;
232    }
233  } else {
234    tracing::info!("Runtime configuration does not change.");
235  }
236
237  Ok(())
238}
239
240async fn get_new_boot_image(
241  backend: &StaticBackendDispatcher,
242  shasta_token: &str,
243  shasta_base_url: &str,
244  shasta_root_cert: &[u8],
245  new_boot_image_configuration_opt: Option<&str>,
246  new_boot_image_id_opt: Option<&str>,
247) -> Result<Option<Image>, Error> {
248  let new_boot_image = if let Some(new_boot_image_configuration) =
249    new_boot_image_configuration_opt
250  {
251    tracing::info!(
252      "Boot configuration '{}' provided",
253      new_boot_image_configuration
254    );
255    let mut image_vec = get_image_vec_related_cfs_configuration_name(
256      backend,
257      shasta_token,
258      shasta_base_url,
259      shasta_root_cert,
260      new_boot_image_configuration.to_string(),
261    )
262    .await?;
263
264    if image_vec.is_empty() {
265      return Err(Error::NotFound(format!(
266        "No boot image found for configuration '{new_boot_image_configuration}'"
267      )));
268    }
269
270    backend.filter_images(&mut image_vec)?;
271
272    let most_recent_image = image_vec.iter().last().ok_or_else(|| {
273      Error::NotFound("No image found for configuration".to_string())
274    })?;
275
276    tracing::debug!(
277      "Boot image id related to configuration '{}' found:\n{:#?}",
278      new_boot_image_configuration,
279      most_recent_image
280    );
281
282    Some(most_recent_image.clone())
283  } else if let Some(boot_image_id) = new_boot_image_id_opt {
284    tracing::info!("Boot image id '{}' provided", boot_image_id);
285    let image_in_csm_vec = backend
286      .get_images(shasta_token, new_boot_image_id_opt)
287      .await?;
288
289    if image_in_csm_vec.is_empty() {
290      return Err(Error::NotFound(format!(
291        "Boot image id '{boot_image_id}' not found"
292      )));
293    }
294
295    image_in_csm_vec.first().cloned()
296  } else {
297    None
298  };
299
300  Ok(new_boot_image)
301}
302
303fn apply_kernel_params(
304  boot_param_vec: &mut [BootParameters],
305  new_kernel_parameters: &str,
306) -> Result<bool, Error> {
307  let mut any_changed = false;
308
309  for boot_parameter in boot_param_vec.iter_mut() {
310    tracing::info!(
311      "Updating '{:?}' kernel parameters to '{}'",
312      boot_parameter.hosts,
313      new_kernel_parameters
314    );
315
316    let changed = boot_parameter.apply_kernel_params(new_kernel_parameters);
317    any_changed = changed || any_changed;
318
319    tracing::info!("need restart? {}", any_changed);
320
321    let image_id = boot_parameter.try_get_boot_image_id().ok_or_else(|| {
322      Error::MissingField(format!(
323        "Could not get boot image id from boot parameters for hosts: {:?}",
324        boot_parameter.hosts
325      ))
326    })?;
327
328    let _ = boot_parameter
329      .update_boot_image(&image_id, &boot_parameter.get_boot_image_etag())?;
330  }
331
332  Ok(any_changed)
333}
334
335async fn collect_boot_images(
336  backend: &StaticBackendDispatcher,
337  shasta_token: &str,
338  boot_param_vec: &mut [BootParameters],
339  new_boot_image_opt: Option<Image>,
340  need_restart: &mut bool,
341) -> Result<HashMap<String, Image>, Error> {
342  let mut image_vec = HashMap::<String, Image>::new();
343
344  if let Some(new_boot_image) = new_boot_image_opt {
345    let new_boot_image_id = new_boot_image
346      .id
347      .as_ref()
348      .ok_or_else(|| {
349        Error::MissingField("New boot image id is missing".to_string())
350      })?
351      .clone();
352
353    let new_boot_image_etag = new_boot_image
354      .link
355      .as_ref()
356      .and_then(|link| link.etag.as_ref())
357      .ok_or_else(|| {
358        Error::MissingField("New boot image etag is missing".to_string())
359      })?;
360
361    image_vec.insert(new_boot_image_id.clone(), new_boot_image.clone());
362
363    let any_differ = boot_param_vec.iter().any(|bp| {
364      bp.try_get_boot_image_id().as_deref() != Some(new_boot_image_id.as_str())
365    });
366
367    if any_differ {
368      for boot_parameter in boot_param_vec.iter_mut() {
369        tracing::info!(
370          "Updating '{:?}' boot image to '{}'",
371          boot_parameter.hosts,
372          new_boot_image_id
373        );
374        boot_parameter
375          .update_boot_image(&new_boot_image_id, new_boot_image_etag)?;
376      }
377      *need_restart = true;
378    }
379  } else {
380    for boot_parameter in boot_param_vec.iter() {
381      let boot_image_id =
382        boot_parameter.try_get_boot_image_id().ok_or_else(|| {
383          Error::MissingField(format!(
384            "Could not get boot image id from boot parameters for hosts: {:?}",
385            boot_parameter.hosts
386          ))
387        })?;
388
389      let boot_image = backend
390        .get_images(shasta_token, Some(boot_image_id.as_str()))
391        .await?
392        .first()
393        .ok_or_else(|| {
394          Error::NotFound("No image found for boot image id".to_string())
395        })?
396        .clone();
397
398      image_vec.insert(boot_image_id, boot_image);
399    }
400  }
401
402  Ok(image_vec)
403}