use manta_backend_dispatcher::{
error::Error,
interfaces::{bss::BootParametersTrait, cfs::CfsTrait, ims::ImsTrait},
types::{
Group,
bss::BootParameters,
ims::{Image, PatchImage},
},
};
use std::collections::HashMap;
use crate::server::common::app_context::InfraContext;
use crate::service::authorization::validate_user_group_members_access;
use crate::service::ims_ops::get_image_vec_related_cfs_configuration_name;
use crate::service::node_ops;
pub use manta_shared::types::api::boot_parameters::{
GetBootParametersParams, UpdateBootParametersParams,
};
pub async fn get_boot_parameters(
infra: &InfraContext<'_>,
token: &str,
params: &GetBootParametersParams,
) -> Result<Vec<BootParameters>, Error> {
tracing::info!("Get boot parameters");
let xname_vec = node_ops::resolve_target_nodes(
infra,
token,
params.host_expression.as_deref(),
params.group_name.as_deref(),
params.settings_group_name.as_deref(),
)
.await?;
validate_user_group_members_access(infra, token, &xname_vec).await?;
if xname_vec.is_empty() {
return Err(Error::BadRequest(
"The list of nodes to operate is empty. Nothing to do".to_string(),
));
}
validate_user_group_members_access(infra, token, &xname_vec).await?;
infra.backend.get_bootparameters(token, &xname_vec).await
}
pub async fn delete_boot_parameters(
infra: &InfraContext<'_>,
token: &str,
hosts: Vec<String>,
) -> Result<(), Error> {
let boot_parameters = BootParameters {
hosts,
macs: None,
nids: None,
params: String::new(),
kernel: String::new(),
initrd: String::new(),
cloud_init: None,
};
validate_user_group_members_access(infra, token, &boot_parameters.hosts)
.await?;
infra
.backend
.delete_bootparameters(token, &boot_parameters)
.await
.map(|_| ())
}
pub async fn add_boot_parameters(
infra: &InfraContext<'_>,
token: &str,
boot_parameters: &BootParameters,
) -> Result<(), Error> {
validate_user_group_members_access(infra, token, &boot_parameters.hosts)
.await?;
infra
.backend
.add_bootparameters(token, boot_parameters)
.await
}
pub async fn update_boot_parameters(
infra: &InfraContext<'_>,
token: &str,
params: UpdateBootParametersParams,
) -> Result<(), Error> {
validate_user_group_members_access(infra, token, ¶ms.hosts).await?;
let boot_parameters = BootParameters {
hosts: params.hosts,
macs: params.macs,
nids: params.nids,
params: params.params,
kernel: params.kernel,
initrd: params.initrd,
cloud_init: None,
};
tracing::debug!("new boot params: {:#?}", boot_parameters);
infra
.backend
.update_bootparameters(token, &boot_parameters)
.await
}
#[derive(serde::Serialize)]
pub(crate) struct BootConfigChangeset {
pub xname_vec: Vec<String>,
pub boot_param_vec: Vec<BootParameters>,
pub image_vec: HashMap<String, Image>,
pub need_restart: bool,
}
pub(crate) async fn prepare_boot_config(
infra: &InfraContext<'_>,
token: &str,
hosts_expression: &str,
new_boot_image_id_opt: Option<&str>,
new_boot_image_configuration_opt: Option<&str>,
new_kernel_parameters_opt: Option<&str>,
) -> Result<BootConfigChangeset, Error> {
let mut need_restart = false;
let xname_vec = node_ops::from_user_hosts_expression_to_xname_vec(
infra,
token,
hosts_expression,
false,
)
.await?;
validate_user_group_members_access(infra, token, &xname_vec).await?;
let mut current_node_boot_param_vec: Vec<BootParameters> =
infra.backend.get_bootparameters(token, &xname_vec).await?;
let new_boot_image_opt = get_new_boot_image(
infra,
token,
new_boot_image_configuration_opt,
new_boot_image_id_opt,
)
.await?;
if let Some(new_kernel_parameters) = new_kernel_parameters_opt {
need_restart |= apply_kernel_params(
&mut current_node_boot_param_vec,
new_kernel_parameters,
)?;
}
let mut image_vec = collect_boot_images(
infra,
token,
&mut current_node_boot_param_vec,
new_boot_image_opt,
&mut need_restart,
)
.await?;
if current_node_boot_param_vec
.first()
.ok_or_else(|| Error::NotFound("No boot parameters found".to_string()))?
.is_root_kernel_param_iscsi_ready()
{
for image in image_vec.values_mut() {
image.set_boot_image_iscsi_ready();
}
}
Ok(BootConfigChangeset {
xname_vec,
boot_param_vec: current_node_boot_param_vec,
image_vec,
need_restart,
})
}
pub(crate) async fn persist_boot_config(
infra: &InfraContext<'_>,
token: &str,
changeset: &BootConfigChangeset,
new_runtime_configuration_opt: Option<&str>,
) -> Result<(), Error> {
tracing::info!("Persist changes");
validate_user_group_members_access(infra, token, &changeset.xname_vec)
.await?;
for boot_parameter in &changeset.boot_param_vec {
tracing::debug!("Updating boot parameter:\n{:#?}", boot_parameter);
let component_patch_rep = infra
.backend
.update_bootparameters(token, boot_parameter)
.await;
tracing::debug!(
"Component boot parameters resp:\n{:#?}",
component_patch_rep
);
}
if let Some(new_runtime_configuration_name) = new_runtime_configuration_opt {
tracing::info!(
"Updating runtime configuration to '{new_runtime_configuration_name}'"
);
infra
.backend
.update_runtime_configuration(
token,
&changeset.xname_vec,
new_runtime_configuration_name,
true,
)
.await?;
for image in changeset.image_vec.values() {
let image_id = image.id.clone().ok_or_else(|| {
Error::MissingField("Image id is missing".to_string())
})?;
let patch_image: PatchImage = image.clone().into();
infra
.backend
.update_image(token, &image_id, &patch_image)
.await?;
}
} else {
tracing::info!("Runtime configuration does not change.");
}
Ok(())
}
async fn get_new_boot_image(
infra: &InfraContext<'_>,
shasta_token: &str,
new_boot_image_configuration_opt: Option<&str>,
new_boot_image_id_opt: Option<&str>,
) -> Result<Option<Image>, Error> {
let new_boot_image = if let Some(new_boot_image_configuration) =
new_boot_image_configuration_opt
{
tracing::info!(
"Boot configuration '{}' provided",
new_boot_image_configuration
);
let mut image_vec = get_image_vec_related_cfs_configuration_name(
infra,
shasta_token,
new_boot_image_configuration.to_string(),
)
.await?;
if image_vec.is_empty() {
return Err(Error::NotFound(format!(
"No boot image found for configuration '{new_boot_image_configuration}'"
)));
}
infra.backend.filter_images(&mut image_vec)?;
let most_recent_image = image_vec.iter().last().ok_or_else(|| {
Error::NotFound("No image found for configuration".to_string())
})?;
tracing::debug!(
"Boot image id related to configuration '{}' found:\n{:#?}",
new_boot_image_configuration,
most_recent_image
);
Some(most_recent_image.clone())
} else if let Some(boot_image_id) = new_boot_image_id_opt {
tracing::info!("Boot image id '{}' provided", boot_image_id);
let image_in_csm_vec = infra
.backend
.get_images(shasta_token, new_boot_image_id_opt)
.await?;
if image_in_csm_vec.is_empty() {
return Err(Error::NotFound(format!(
"Boot image id '{boot_image_id}' not found"
)));
}
image_in_csm_vec.first().cloned()
} else {
None
};
Ok(new_boot_image)
}
fn apply_kernel_params(
boot_param_vec: &mut [BootParameters],
new_kernel_parameters: &str,
) -> Result<bool, Error> {
tracing::info!(
"Updating kernel parameters to '{}' across {} boot-parameter record(s)",
new_kernel_parameters,
boot_param_vec.len()
);
let mut any_changed = false;
for boot_parameter in boot_param_vec.iter_mut() {
tracing::debug!(
"Updating '{:?}' kernel parameters to '{}'",
boot_parameter.hosts,
new_kernel_parameters
);
let changed = boot_parameter.apply_kernel_params(new_kernel_parameters);
any_changed = changed || any_changed;
let image_id = boot_parameter.try_get_boot_image_id().ok_or_else(|| {
Error::MissingField(format!(
"Could not get boot image id from boot parameters for hosts: {:?}",
boot_parameter.hosts
))
})?;
boot_parameter
.update_boot_image(&image_id, &boot_parameter.get_boot_image_etag())?;
}
Ok(any_changed)
}
async fn collect_boot_images(
infra: &InfraContext<'_>,
shasta_token: &str,
boot_param_vec: &mut [BootParameters],
new_boot_image_opt: Option<Image>,
need_restart: &mut bool,
) -> Result<HashMap<String, Image>, Error> {
let mut image_vec = HashMap::<String, Image>::new();
if let Some(new_boot_image) = new_boot_image_opt {
let new_boot_image_id = new_boot_image
.id
.as_ref()
.ok_or_else(|| {
Error::MissingField("New boot image id is missing".to_string())
})?
.clone();
let new_boot_image_etag = new_boot_image
.link
.as_ref()
.and_then(|link| link.etag.as_ref())
.ok_or_else(|| {
Error::MissingField("New boot image etag is missing".to_string())
})?;
image_vec.insert(new_boot_image_id.clone(), new_boot_image.clone());
let any_differ = boot_param_vec.iter().any(|bp| {
bp.try_get_boot_image_id().as_deref() != Some(new_boot_image_id.as_str())
});
if any_differ {
tracing::info!(
"Updating boot image to '{}' across {} boot-parameter record(s)",
new_boot_image_id,
boot_param_vec.len()
);
for boot_parameter in boot_param_vec.iter_mut() {
tracing::debug!(
"Updating '{:?}' boot image to '{}'",
boot_parameter.hosts,
new_boot_image_id
);
boot_parameter
.update_boot_image(&new_boot_image_id, new_boot_image_etag)?;
}
*need_restart = true;
}
} else {
let mut unique_ids: Vec<String> = Vec::new();
let mut seen: std::collections::HashSet<String> =
std::collections::HashSet::new();
for boot_parameter in boot_param_vec.iter() {
let boot_image_id =
boot_parameter.try_get_boot_image_id().ok_or_else(|| {
Error::MissingField(format!(
"Could not get boot image id from boot parameters for hosts: {:?}",
boot_parameter.hosts
))
})?;
if seen.insert(boot_image_id.clone()) {
unique_ids.push(boot_image_id);
}
}
let fetched: Vec<(String, Image)> =
futures::future::try_join_all(unique_ids.iter().map(|id| async move {
let image = infra
.backend
.get_images(shasta_token, Some(id.as_str()))
.await?
.first()
.ok_or_else(|| {
Error::NotFound(format!("No image found for boot image id '{id}'"))
})?
.clone();
Ok::<_, Error>((id.clone(), image))
}))
.await?;
for (id, image) in fetched {
image_vec.insert(id, image);
}
}
Ok(image_vec)
}
pub fn get_restricted_boot_parameters(
group_available_vec: &[Group],
boot_parameter_vec: &[BootParameters],
) -> Vec<BootParameters> {
let group_members: Vec<String> = group_available_vec
.iter()
.flat_map(Group::get_members)
.collect();
boot_parameter_vec
.iter()
.filter(|boot_param| {
group_members
.iter()
.any(|gma| boot_param.hosts.contains(gma))
})
.cloned()
.collect::<Vec<BootParameters>>()
}
#[cfg(test)]
mod tests {
use super::*;
use manta_backend_dispatcher::types::Member;
fn make_group(label: &str, member_ids: Vec<&str>) -> Group {
Group {
label: label.to_string(),
description: None,
tags: None,
members: Some(Member {
ids: Some(member_ids.into_iter().map(String::from).collect()),
}),
exclusive_group: None,
}
}
fn make_boot_params(hosts: Vec<&str>) -> BootParameters {
BootParameters {
hosts: hosts.into_iter().map(String::from).collect(),
..Default::default()
}
}
#[test]
fn filters_boot_params_by_group_membership() {
let groups =
vec![make_group("grp1", vec!["x1000c0s0b0n0", "x1000c0s0b0n1"])];
let boot_params = vec![
make_boot_params(vec!["x1000c0s0b0n0"]),
make_boot_params(vec!["x9999c0s0b0n0"]),
make_boot_params(vec!["x1000c0s0b0n1"]),
];
let result = get_restricted_boot_parameters(&groups, &boot_params);
assert_eq!(result.len(), 2);
assert_eq!(result[0].hosts, vec!["x1000c0s0b0n0"]);
assert_eq!(result[1].hosts, vec!["x1000c0s0b0n1"]);
}
#[test]
fn returns_empty_when_no_group_members_match() {
let groups = vec![make_group("grp1", vec!["x1000c0s0b0n0"])];
let boot_params = vec![make_boot_params(vec!["x9999c0s0b0n0"])];
let result = get_restricted_boot_parameters(&groups, &boot_params);
assert!(result.is_empty());
}
#[test]
fn returns_empty_when_groups_are_empty() {
let boot_params = vec![make_boot_params(vec!["x1000c0s0b0n0"])];
let result = get_restricted_boot_parameters(&[], &boot_params);
assert!(result.is_empty());
}
#[test]
fn returns_empty_when_boot_params_are_empty() {
let groups = vec![make_group("grp1", vec!["x1000c0s0b0n0"])];
let result = get_restricted_boot_parameters(&groups, &[]);
assert!(result.is_empty());
}
#[test]
fn aggregates_members_across_multiple_groups() {
let groups = vec![
make_group("grp1", vec!["x1000c0s0b0n0"]),
make_group("grp2", vec!["x2000c0s0b0n0"]),
];
let boot_params = vec![
make_boot_params(vec!["x1000c0s0b0n0"]),
make_boot_params(vec!["x2000c0s0b0n0"]),
make_boot_params(vec!["x3000c0s0b0n0"]),
];
let result = get_restricted_boot_parameters(&groups, &boot_params);
assert_eq!(result.len(), 2);
}
}