Skip to content

Large diffs are not rendered by default.

119 changes: 73 additions & 46 deletions nexus/reconfigurator/planning/src/mgs_updates/host_phase_1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use nexus_types::deployment::PendingMgsUpdate;
use nexus_types::deployment::PendingMgsUpdateDetails;
use nexus_types::deployment::PendingMgsUpdateHostPhase1Details;
use nexus_types::deployment::planning_report::FailedHostOsUpdateReason;
use nexus_types::deployment::planning_report::HostOsTargetArtifactError;
use nexus_types::inventory::Collection;
use nexus_types::inventory::SpType;
use nexus_types::tuf_repo::TufRepoDescription;
Expand Down Expand Up @@ -377,52 +378,10 @@ pub(super) fn try_make_update(
);
};

let Some(sled_type) = OxideSled::try_from_model(&baseboard_id.part_number)
else {
return Err(FailedHostOsUpdateReason::UnableToDetermineSledModel(
baseboard_id.part_number.clone(),
));
};

let phase_1_artifact = current_artifacts.artifacts.get_only(
&OsPhase1Tags {
os_board: match sled_type {
OxideSled::Gimlet => OsBoard::GIMLET,
OxideSled::Cosmo => OsBoard::COSMO,
},
os_variant: OsVariant::Host,
}
.into(),
);
let phase_2_artifact = current_artifacts
.artifacts
.get_only(&OsPhase2Tags { os_variant: OsVariant::Host }.into());
let (phase_1_artifact, phase_2_artifact) =
match (phase_1_artifact, phase_2_artifact) {
// Common case: Exactly 1 of each artifact.
(Ok(p1), Ok(p2)) => (p1, p2),
// "TUF is broken" cases: missing both, one or the other.
(Err(GetError::NotFound), Err(GetError::NotFound)) => {
return Err(FailedHostOsUpdateReason::NoMatchingArtifactsFound);
}
(Err(GetError::NotFound), Ok(_)) => {
return Err(
FailedHostOsUpdateReason::NoMatchingPhase1ArtifactFound,
);
}
(Ok(_), Err(GetError::NotFound)) => {
return Err(
FailedHostOsUpdateReason::NoMatchingPhase2ArtifactFound,
);
}
// "TUF is broken" cases: have multiple of one or the other. This
// should be impossible unless we shipped a TUF repo with multiple
// host OS images. We can't proceed, because we don't know how to
// pair up which phase 1 matches which phase 2.
(Err(GetError::TooMany), _) | (_, Err(GetError::TooMany)) => {
return Err(FailedHostOsUpdateReason::TooManyMatchingArtifacts);
}
};
let HostOsTargetArtifacts {
phase_1: phase_1_artifact,
phase_2: phase_2_artifact,
} = HostOsTargetArtifacts::for_board(baseboard_id, current_artifacts)?;

// If the artifact matches what's deployed, then no update is needed. We
// only need to look at the running phase 2; that tells us what we're
Expand Down Expand Up @@ -481,6 +440,74 @@ pub(super) fn try_make_update(
))
}

/// Host OS phase 1 and phase 2 artifacts that a TUF repo specifies for a sled
pub(super) struct HostOsTargetArtifacts<'a> {
pub(super) phase_1: &'a Artifact,
pub(super) phase_2: &'a Artifact,
}

impl<'a> HostOsTargetArtifacts<'a> {
/// Finds the host OS artifacts in `current_artifacts` that apply to the
/// sled identified by `baseboard_id`
pub(super) fn for_board(
baseboard_id: &BaseboardId,
current_artifacts: &'a TufRepoDescription,
) -> Result<Self, HostOsTargetArtifactError> {
let Some(sled_type) =
OxideSled::try_from_model(&baseboard_id.part_number)
else {
return Err(HostOsTargetArtifactError::UnableToDetermineSledModel(
baseboard_id.part_number.clone(),
));
};

let phase_1_artifact = current_artifacts.artifacts.get_only(
&OsPhase1Tags {
os_board: match sled_type {
OxideSled::Gimlet => OsBoard::GIMLET,
OxideSled::Cosmo => OsBoard::COSMO,
},
os_variant: OsVariant::Host,
}
.into(),
);
let phase_2_artifact = current_artifacts
.artifacts
.get_only(&OsPhase2Tags { os_variant: OsVariant::Host }.into());
let (phase_1, phase_2) = match (phase_1_artifact, phase_2_artifact) {
// Common case: Exactly 1 of each artifact.
(Ok(p1), Ok(p2)) => (p1, p2),
// "TUF is broken" cases: missing both, one or the other.
(Err(GetError::NotFound), Err(GetError::NotFound)) => {
return Err(
HostOsTargetArtifactError::NoMatchingArtifactsFound,
);
}
(Err(GetError::NotFound), Ok(_)) => {
return Err(
HostOsTargetArtifactError::NoMatchingPhase1ArtifactFound,
);
}
(Ok(_), Err(GetError::NotFound)) => {
return Err(
HostOsTargetArtifactError::NoMatchingPhase2ArtifactFound,
);
}
// "TUF is broken" cases: have multiple of one or the other. This
// should be impossible unless we shipped a TUF repo with multiple
// host OS images. We can't proceed, because we don't know how to
// pair up which phase 1 matches which phase 2.
(Err(GetError::TooMany), _) | (_, Err(GetError::TooMany)) => {
return Err(
HostOsTargetArtifactError::TooManyMatchingArtifacts,
);
}
};

Ok(HostOsTargetArtifacts { phase_1, phase_2 })
}
}

#[cfg(test)]
mod tests {
use crate::mgs_updates::EvacuatingSleds;
Expand Down
142 changes: 140 additions & 2 deletions nexus/reconfigurator/planning/src/mgs_updates/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

//! Facilities for making choices about MGS-managed updates

mod deployed_sled_versions;
mod host_phase_1;
mod rot;
mod rot_bootloader;
Expand Down Expand Up @@ -44,6 +45,11 @@ use thiserror::Error;
use tufaceous_artifact::ArtifactVersion;
use tufaceous_artifact::ArtifactVersionError;

pub use deployed_sled_versions::ActiveImageCheck;
pub use deployed_sled_versions::DeployedSledVersions;
pub use deployed_sled_versions::DeployedSledVersionsError;
pub use deployed_sled_versions::ImageCheckError;
pub use deployed_sled_versions::RotCheckError;
pub(crate) use host_phase_1::PendingHostPhase2Changes;
pub(crate) use sled_evacuation::EvacuatingSleds;
pub(crate) use sled_evacuation::PendingUpdateDispositionChanges;
Expand Down Expand Up @@ -965,10 +971,14 @@ mod test {
use nexus_types::deployment::planning_report::FailedSpUpdateReason;
use nexus_types::deployment::planning_report::WaitingOnSledEvacuationDetails;
use nexus_types::deployment::planning_report::ZoneUnsafeToShutdown;
use nexus_types::inventory::Caboose;
use nexus_types::inventory::CabooseVersionError;
use nexus_types::inventory::CabooseWhich;
use nexus_types::inventory::Collection;
use nexus_types::inventory::SpType;
use omicron_generation_kinds::SledConfigGeneration;
use omicron_test_utils::dev::LogContext;
use omicron_test_utils::dev::test_setup_log;
use omicron_uuid_kinds::OmicronZoneUuid;
use omicron_uuid_kinds::SledUuid;
use sled_hardware_types::BaseboardId;
Expand Down Expand Up @@ -1139,8 +1149,10 @@ mod test {
expected_blocked_updates.push(BlockedMgsUpdate {
baseboard_id: baseboard_id.clone(),
reason: FailedMgsUpdateReason::Sp(
FailedSpUpdateReason::CabooseNotInInventory(
CabooseWhich::SpSlot0,
FailedSpUpdateReason::Caboose(
CabooseVersionError::NotInInventory(
CabooseWhich::SpSlot0,
),
),
),
});
Expand Down Expand Up @@ -1200,6 +1212,132 @@ mod test {
assert!(updates.is_empty());
}

// Replaces the version in caboose `which` on every board in `collection`.
fn set_caboose_version(
collection: &mut Collection,
which: CabooseWhich,
version: &str,
) {
let cabooses = collection
.cabooses_found
.get_mut(&which)
.expect("test collection contains requested cabooses");
for found in cabooses.values_mut() {
let mut caboose = Caboose::clone(&found.caboose);
caboose.version = version.to_string();
found.caboose = Arc::new(caboose);
}
}

// Confirm that the planner reports caboose versions that cannot be parsed,
// both for the active slot (needed to decide whether an update is needed)
// and for the inactive slot (needed to configure the update).
#[test]
fn test_unparseable_caboose_version() {
let test_name = "planning_mgs_updates_unparseable_caboose_version";
let logctx = test_setup_log(test_name);
let log = &logctx.log;
let test_boards = TestBoards::new(test_name);
let planner_config = PlannerConfig::default();
let repo = test_boards.tuf_repo();
let current_updates = PendingMgsUpdates::new();
let plan = |collection: &Collection| {
MgsUpdatePlanner {
log,
planner_config: &planner_config,
inventory: collection,
current_boards: &UpdateableBoard::all_from_collection(
collection,
),
zone_safety_checks: &ZoneSafetyChecks::empty(),
evacuating_sleds: &EvacuatingSleds::empty(),
current_updates: &current_updates,
current_artifacts: &TargetReleaseDescription::TufRepo(
repo.clone(),
),
nmax_updates: 1,
impossible_update_policy: ImpossibleUpdatePolicy::Reevaluate,
}
.plan()
};

// `ArtifactVersion` does not accept an empty string, so we use that as
// a version that cannot be parsed.
let bad_version_str = "";

// An unparseable active SP version blocks an SP update on every board.
let mut collection = test_boards.collection_builder().build();
set_caboose_version(
&mut collection,
CabooseWhich::SpSlot0,
bad_version_str,
);
let PlannedMgsUpdates {
pending_updates: updates,
blocked_mgs_updates,
..
} = plan(&collection);
assert!(updates.is_empty());
assert_eq!(blocked_mgs_updates.len(), collection.baseboards.len());
for blocked in &blocked_mgs_updates {
assert!(
matches!(
&blocked.reason,
FailedMgsUpdateReason::Sp(FailedSpUpdateReason::Caboose(
CabooseVersionError::Parse {
caboose: CabooseWhich::SpSlot0,
..
}
))
),
"unexpected reason: {:?}",
blocked.reason
);
}

// Test an unparseable inactive SP version. This has no impact unless a
// board needs an SP update. Here, we set up sled 0 to need an update.
// Then we verify that a problem is reported because its inactive
// version cannot be parsed and that no problem is reported for any
// other board. By default, test boards have no inactive SP caboose,
// so we create one first.
let mut collection = test_boards
.collection_builder()
.sp_versions(
ARTIFACT_VERSION_2,
ExpectedVersion::Version(ARTIFACT_VERSION_1),
)
.sp_active_version_exception(SpType::Sled, 0, ARTIFACT_VERSION_1)
.build();
set_caboose_version(
&mut collection,
CabooseWhich::SpSlot1,
bad_version_str,
);
let PlannedMgsUpdates {
pending_updates: updates,
blocked_mgs_updates,
..
} = plan(&collection);
assert!(updates.is_empty());
assert_eq!(blocked_mgs_updates.len(), 1, "{blocked_mgs_updates:?}");
assert!(
matches!(
&blocked_mgs_updates[0].reason,
FailedMgsUpdateReason::Sp(FailedSpUpdateReason::Caboose(
CabooseVersionError::Parse {
caboose: CabooseWhich::SpSlot1,
..
}
))
),
"unexpected reason: {:?}",
blocked_mgs_updates[0].reason
);

logctx.cleanup_successful();
}

// Confirm our behavior for impossible updates
#[test]
fn test_impossible_update_policy() {
Expand Down
Loading
Loading