diff --git a/dev-tools/reconfigurator-cli/src/lib.rs b/dev-tools/reconfigurator-cli/src/lib.rs index ad5ff37f31d..f80e79d0596 100644 --- a/dev-tools/reconfigurator-cli/src/lib.rs +++ b/dev-tools/reconfigurator-cli/src/lib.rs @@ -2134,8 +2134,11 @@ fn cmd_blueprint_blippy( let resolved_id = state.system().resolve_blueprint_id(args.blueprint_id.into())?; let blueprint = state.system().get_blueprint(&resolved_id)?; - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Severity); + let planning_input = sim + .planning_input(blueprint) + .context("failed to construct planning input")?; + let report = Blippy::new(&blueprint, &planning_input) + .into_report(BlippyReportSortKey::Severity); Ok(Some(format!("{}", report.display()))) } diff --git a/nexus/reconfigurator/blippy/src/blippy.rs b/nexus/reconfigurator/blippy/src/blippy.rs index 972b388833e..28b4f8ebc22 100644 --- a/nexus/reconfigurator/blippy/src/blippy.rs +++ b/nexus/reconfigurator/blippy/src/blippy.rs @@ -10,6 +10,9 @@ use nexus_types::deployment::Blueprint; use nexus_types::deployment::BlueprintArtifactVersion; use nexus_types::deployment::BlueprintDatasetConfig; use nexus_types::deployment::BlueprintZoneConfig; +use nexus_types::deployment::OmicronZoneExternalIp; +use nexus_types::deployment::OmicronZoneNicEntry; +use nexus_types::deployment::PlanningInput; use nexus_types::inventory::ZpoolName; use omicron_common::address::DnsSubnet; use omicron_common::address::Ipv6Subnet; @@ -55,6 +58,7 @@ impl fmt::Display for Severity { pub enum Kind { Blueprint(BlueprintKind), Sled { sled_id: SledUuid, kind: Box }, + PlanningInput(PlanningInputKind), } impl Kind { @@ -62,6 +66,7 @@ impl Kind { enum Component<'a> { Blueprint, Sled(&'a SledUuid), + PlanningInput, } impl fmt::Display for Component<'_> { @@ -69,13 +74,15 @@ impl Kind { match self { Component::Blueprint => write!(f, "blueprint"), Component::Sled(id) => write!(f, "sled {id}"), + Component::PlanningInput => write!(f, "planning input"), } } } match self { - Kind::Blueprint { .. } => Component::Blueprint, + Kind::Blueprint(_) => Component::Blueprint, Kind::Sled { sled_id, .. } => Component::Sled(sled_id), + Kind::PlanningInput(_) => Component::PlanningInput, } } @@ -83,6 +90,7 @@ impl Kind { enum Subkind<'a> { Blueprint(&'a BlueprintKind), Sled(&'a SledKind), + PlanningInput(&'a PlanningInputKind), } impl fmt::Display for Subkind<'_> { @@ -90,6 +98,7 @@ impl Kind { match self { Subkind::Blueprint(kind) => write!(f, "{kind}"), Subkind::Sled(kind) => write!(f, "{kind}"), + Subkind::PlanningInput(kind) => write!(f, "{kind}"), } } } @@ -97,6 +106,7 @@ impl Kind { match self { Kind::Blueprint(kind) => Subkind::Blueprint(kind), Kind::Sled { kind, .. } => Subkind::Sled(kind), + Kind::PlanningInput(kind) => Subkind::PlanningInput(kind), } } } @@ -488,6 +498,54 @@ impl fmt::Display for SledKind { } } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum PlanningInputKind { + IpNotInBlueprint(OmicronZoneExternalIp), + NicMacNotInBluperint(OmicronZoneNicEntry), + NicIpNotInBlueprint(OmicronZoneNicEntry), + NicWithUnknownOpteSubnet(OmicronZoneNicEntry), +} + +impl fmt::Display for PlanningInputKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PlanningInputKind::IpNotInBlueprint(ip) => { + write!( + f, + "planning input contains an external IP \ + not described by the blueprint: {} ({})", + ip.ip(), + ip.id() + ) + } + PlanningInputKind::NicMacNotInBluperint(nic) => { + write!( + f, + "planning input contains a NIC with a MAC address \ + not described by the blueprint: {} (NIC {} in zone {})", + nic.nic.mac, nic.nic.id, nic.zone_id, + ) + } + PlanningInputKind::NicIpNotInBlueprint(nic) => { + write!( + f, + "planning input contains a NIC with an IP address \ + not described by the blueprint: {} (NIC {} in zone {})", + nic.nic.ip, nic.nic.id, nic.zone_id, + ) + } + PlanningInputKind::NicWithUnknownOpteSubnet(nic) => { + write!( + f, + "planning input contains a NIC with an IP not in a known + OPTE subnet: {} (NIC {} in zone {})", + nic.nic.ip, nic.nic.id, nic.zone_id, + ) + } + } + } +} + impl Note { pub fn display(&self, sort_key: BlippyReportSortKey) -> NoteDisplay<'_> { NoteDisplay { note: self, sort_key } @@ -532,7 +590,20 @@ pub struct Blippy<'a> { } impl<'a> Blippy<'a> { - pub fn new(blueprint: &'a Blueprint) -> Self { + /// Check `blueprint` for internal inconsistencies and check for + /// inconsistencies between `blueprint` and `planning_input`. + pub fn new( + blueprint: &'a Blueprint, + planning_input: &PlanningInput, + ) -> Self { + let mut slf = Self { blueprint, notes: Vec::new() }; + checks::perform_all_blueprint_only_checks(&mut slf); + checks::perform_planning_input_checks(&mut slf, planning_input); + slf + } + + /// Check `blueprint` for internal inconsistencies. + pub fn new_blueprint_only(blueprint: &'a Blueprint) -> Self { let mut slf = Self { blueprint, notes: Vec::new() }; checks::perform_all_blueprint_only_checks(&mut slf); slf @@ -562,6 +633,14 @@ impl<'a> Blippy<'a> { }); } + pub(crate) fn push_planning_input_note( + &mut self, + severity: Severity, + kind: PlanningInputKind, + ) { + self.notes.push(Note { severity, kind: Kind::PlanningInput(kind) }); + } + pub fn into_report( self, sort_key: BlippyReportSortKey, diff --git a/nexus/reconfigurator/blippy/src/checks.rs b/nexus/reconfigurator/blippy/src/checks.rs index a2d2016298e..db66d337180 100644 --- a/nexus/reconfigurator/blippy/src/checks.rs +++ b/nexus/reconfigurator/blippy/src/checks.rs @@ -4,6 +4,7 @@ use crate::blippy::Blippy; use crate::blippy::BlueprintKind; +use crate::blippy::PlanningInputKind; use crate::blippy::Severity; use crate::blippy::SledKind; use nexus_sled_agent_shared::inventory::ZoneKind; @@ -17,6 +18,7 @@ use nexus_types::deployment::BlueprintZoneDisposition; use nexus_types::deployment::BlueprintZoneImageSource; use nexus_types::deployment::BlueprintZoneType; use nexus_types::deployment::OmicronZoneExternalIp; +use nexus_types::deployment::PlanningInput; use nexus_types::deployment::SledFilter; use nexus_types::deployment::blueprint_zone_type; use omicron_common::address::DnsSubnet; @@ -31,8 +33,16 @@ use omicron_uuid_kinds::ZpoolUuid; use std::collections::BTreeMap; use std::collections::BTreeSet; use std::collections::btree_map::Entry; +use std::net::IpAddr; use std::net::Ipv6Addr; +pub(crate) fn perform_planning_input_checks( + blippy: &mut Blippy<'_>, + input: &PlanningInput, +) { + check_planning_input_network_records_appear_in_blueprint(blippy, input); +} + pub(crate) fn perform_all_blueprint_only_checks(blippy: &mut Blippy<'_>) { check_underlay_ips(blippy); check_external_networking(blippy); @@ -750,8 +760,8 @@ mod tests { let logctx = test_setup_log(TEST_NAME); let (_, _, blueprint) = example(&logctx.log, TEST_NAME); - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); if !report.notes().is_empty() { eprintln!("{}", report.display()); panic!("example blueprint should have no blippy notes"); @@ -844,8 +854,8 @@ mod tests { }, ]; - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); for note in expected_notes { assert!( @@ -931,8 +941,8 @@ mod tests { mem::drop(dns0); mem::drop(dns1); - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); assert!( report.notes().contains(&expected_note), @@ -1001,8 +1011,8 @@ mod tests { mem::drop(nexus0); mem::drop(nexus1); - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); for note in expected_notes { assert!( @@ -1068,8 +1078,8 @@ mod tests { mem::drop(nexus0); mem::drop(nexus1); - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); for note in expected_notes { assert!( @@ -1135,8 +1145,8 @@ mod tests { mem::drop(nexus0); mem::drop(nexus1); - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); for note in expected_notes { assert!( @@ -1218,8 +1228,8 @@ mod tests { }, ]; - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); for note in expected_notes { assert!( @@ -1293,8 +1303,8 @@ mod tests { }, ]; - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); for note in expected_notes { assert!( @@ -1352,8 +1362,8 @@ mod tests { }, }]; - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); for note in expected_notes { assert!( @@ -1397,8 +1407,8 @@ mod tests { } assert!(found_duplicate); - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); for note in report.notes() { match ¬e.kind { @@ -1481,8 +1491,8 @@ mod tests { }, ]; - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); for note in expected_notes { assert!( @@ -1556,8 +1566,8 @@ mod tests { }, ]; - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); for note in expected_notes { assert!( @@ -1628,8 +1638,8 @@ mod tests { }) .collect::>(); - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); for note in expected_notes { assert!( @@ -1700,8 +1710,8 @@ mod tests { .collect::>(); assert!(!expected_notes.is_empty()); - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); for note in expected_notes { assert!( @@ -1802,8 +1812,8 @@ mod tests { // We should have modified 3 datasets. assert_eq!(expected_notes.len(), 3); - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); for note in expected_notes { assert!( @@ -1917,8 +1927,8 @@ mod tests { }, ]; - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); assert_eq!(report.notes(), &expected_notes); @@ -1944,8 +1954,8 @@ mod tests { ), }]; - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); assert_eq!(report.notes(), &expected_notes); @@ -2047,11 +2057,171 @@ mod tests { }, }]; - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); eprintln!("{}", report.display()); assert_eq!(report.notes(), &expected_notes); logctx.cleanup_successful(); } } + +// For a given `PlanningInput` / `Blueprint` pair that could be passed to the +// planner, there should never be any external networking resources in the +// planning input (which is derived from the contents of CRDB) that we don't +// know about from the parent blueprint. It's possible a given planning +// iteration could see such a state if there have been intermediate changes made +// by other Nexus instances; e.g., +// +// 1. Nexus A generates a `PlanningInput` by reading from CRDB +// 2. Nexus B executes on a target blueprint that removes IPs/NICs from +// CRDB +// 3. Nexus B regenerates a new blueprint and prunes the zone(s) associated +// with the IPs/NICs from step 2 +// 4. Nexus B makes this new blueprint the target +// 5. Nexus A attempts to run planning with its `PlanningInput` from step 1 but +// the target blueprint from step 4; this will fail the following checks +// because the input contains records that were removed in step 3 +// +// We do not need to handle this class of error; it's a transient failure that +// will clear itself up when Nexus A repeats its planning loop from the top and +// generates a new `PlanningInput`. +// +// There may still be database records corresponding to _expunged_ zones, but +// that's okay: it just means we haven't yet realized a blueprint where those +// zones are expunged. And those should should still be in the blueprint (not +// pruned) until their database records are cleaned up. +// +// It's also possible that there may be networking records in the database +// assigned to zones that have been expunged, and the blueprint uses those same +// records for new zones. This is also fine and expected, and is a similar case +// to the previous paragraph: a zone with networking resources was expunged, the +// database doesn't realize it yet, but can still move forward and make planning +// decisions that reuse those resources for new zones. +fn check_planning_input_network_records_appear_in_blueprint( + blippy: &mut Blippy<'_>, + input: &PlanningInput, +) { + use nexus_types::deployment::OmicronZoneExternalIp; + use omicron_common::address::DNS_OPTE_IPV4_SUBNET; + use omicron_common::address::DNS_OPTE_IPV6_SUBNET; + use omicron_common::address::NEXUS_OPTE_IPV4_SUBNET; + use omicron_common::address::NEXUS_OPTE_IPV6_SUBNET; + use omicron_common::address::NTP_OPTE_IPV4_SUBNET; + use omicron_common::address::NTP_OPTE_IPV6_SUBNET; + use omicron_common::api::external::MacAddr; + + let mut all_macs: BTreeSet = BTreeSet::new(); + let mut all_nexus_nic_ips: BTreeSet = BTreeSet::new(); + let mut all_boundary_ntp_nic_ips: BTreeSet = BTreeSet::new(); + let mut all_external_dns_nic_ips: BTreeSet = BTreeSet::new(); + let mut all_external_ips: BTreeSet = BTreeSet::new(); + + // Unlike the construction of the external IP allocator and existing IPs + // constructed above in `BuilderExternalNetworking::new()`, we do not + // check for duplicates here: we could very well see reuse of IPs + // between expunged zones or between expunged -> running zones. + for (_, z) in + blippy.blueprint().all_omicron_zones(BlueprintZoneDisposition::any) + { + let zone_type = &z.zone_type; + match zone_type { + BlueprintZoneType::BoundaryNtp(ntp) => { + all_boundary_ntp_nic_ips.insert(ntp.nic.ip); + } + BlueprintZoneType::Nexus(nexus) => { + all_nexus_nic_ips.insert(nexus.nic.ip); + } + BlueprintZoneType::ExternalDns(dns) => { + all_external_dns_nic_ips.insert(dns.nic.ip); + } + _ => (), + } + + if let Some((external_ip, nic)) = zone_type.external_networking() { + // Ignore localhost (used by the test suite). + if !external_ip.ip().is_loopback() { + all_external_ips.insert(external_ip); + } + all_macs.insert(nic.mac); + } + } + for external_ip_entry in + input.network_resources().omicron_zone_external_ips() + { + // As above, ignore localhost (used by the test suite). + if external_ip_entry.ip.ip().is_loopback() { + continue; + } + if !all_external_ips.contains(&external_ip_entry.ip) { + blippy.push_planning_input_note( + Severity::Fatal, + PlanningInputKind::IpNotInBlueprint(external_ip_entry.ip), + ); + } + } + for nic_entry in input.network_resources().omicron_zone_nics() { + if !all_macs.contains(&nic_entry.nic.mac) { + blippy.push_planning_input_note( + Severity::Fatal, + PlanningInputKind::NicMacNotInBluperint(nic_entry), + ); + } + match nic_entry.nic.ip { + IpAddr::V4(ip) if NEXUS_OPTE_IPV4_SUBNET.contains(ip) => { + if !all_nexus_nic_ips.contains(&ip.into()) { + blippy.push_planning_input_note( + Severity::Fatal, + PlanningInputKind::NicIpNotInBlueprint(nic_entry), + ); + } + } + IpAddr::V4(ip) if NTP_OPTE_IPV4_SUBNET.contains(ip) => { + if !all_boundary_ntp_nic_ips.contains(&ip.into()) { + blippy.push_planning_input_note( + Severity::Fatal, + PlanningInputKind::NicIpNotInBlueprint(nic_entry), + ); + } + } + IpAddr::V4(ip) if DNS_OPTE_IPV4_SUBNET.contains(ip) => { + if !all_external_dns_nic_ips.contains(&ip.into()) { + blippy.push_planning_input_note( + Severity::Fatal, + PlanningInputKind::NicIpNotInBlueprint(nic_entry), + ); + } + } + IpAddr::V6(ip) if NEXUS_OPTE_IPV6_SUBNET.contains(ip) => { + if !all_nexus_nic_ips.contains(&ip.into()) { + blippy.push_planning_input_note( + Severity::Fatal, + PlanningInputKind::NicIpNotInBlueprint(nic_entry), + ); + } + } + IpAddr::V6(ip) if NTP_OPTE_IPV6_SUBNET.contains(ip) => { + if !all_boundary_ntp_nic_ips.contains(&ip.into()) { + blippy.push_planning_input_note( + Severity::Fatal, + PlanningInputKind::NicIpNotInBlueprint(nic_entry), + ); + } + } + IpAddr::V6(ip) if DNS_OPTE_IPV6_SUBNET.contains(ip) => { + if !all_external_dns_nic_ips.contains(&ip.into()) { + blippy.push_planning_input_note( + Severity::Fatal, + PlanningInputKind::NicIpNotInBlueprint(nic_entry), + ); + } + } + _ => { + blippy.push_planning_input_note( + Severity::Fatal, + PlanningInputKind::NicWithUnknownOpteSubnet(nic_entry), + ); + } + } + } +} diff --git a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs index 7113d12ba8a..ab4fcc981cf 100644 --- a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs +++ b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs @@ -2803,9 +2803,12 @@ pub mod test { /// Checks various conditions that should be true for all blueprints #[track_caller] - pub fn verify_blueprint(blueprint: &Blueprint) { - let blippy_report = - Blippy::new(blueprint).into_report(BlippyReportSortKey::Kind); + pub fn verify_blueprint( + blueprint: &Blueprint, + planning_input: &PlanningInput, + ) { + let blippy_report = Blippy::new(blueprint, planning_input) + .into_report(BlippyReportSortKey::Kind); if !blippy_report.notes().is_empty() { eprintln!("{}", blueprint.display()); eprintln!("---"); @@ -2824,7 +2827,7 @@ pub mod test { rng.next_system_rng(), ) .build(); - verify_blueprint(&blueprint1); + verify_blueprint(&blueprint1, &example.input); let mut builder = BlueprintBuilder::new_based_on( &logctx.log, @@ -2861,7 +2864,7 @@ pub mod test { } let blueprint2 = builder.build(BlueprintSource::Test); - verify_blueprint(&blueprint2); + verify_blueprint(&blueprint2, &example.input); let summary = blueprint2.diff_since_blueprint(&blueprint1); println!( "initial blueprint -> next blueprint (expected no changes):\n{}", @@ -2910,7 +2913,7 @@ pub mod test { builder.sled_ensure_zone_datasets(new_sled_id).unwrap(); let blueprint3 = builder.build(BlueprintSource::Test); - verify_blueprint(&blueprint3); + verify_blueprint(&blueprint3, &input); let summary = blueprint3.diff_since_blueprint(&blueprint2); println!( "expecting new NTP and Crucible zones:\n{}", @@ -3005,7 +3008,7 @@ pub mod test { let mut rng = SimRngState::from_seed(TEST_NAME); let (collection, input, mut blueprint1) = example(&logctx.log, TEST_NAME); - verify_blueprint(&blueprint1); + verify_blueprint(&blueprint1, &input); // Mark one sled as having a desired state of decommissioned. let decommision_sled_id = @@ -3057,7 +3060,7 @@ pub mod test { ) .expect("created builder") .build(BlueprintSource::Test); - verify_blueprint(&blueprint2); + verify_blueprint(&blueprint2, &input); // We carried forward the desired state. assert_eq!( @@ -3095,7 +3098,7 @@ pub mod test { ) .expect("created builder") .build(BlueprintSource::Test); - verify_blueprint(&blueprint3); + verify_blueprint(&blueprint3, &input); assert_eq!( blueprint3.sleds.get(&decommision_sled_id).map(|c| c.state), Some(SledState::Decommissioned), @@ -3221,7 +3224,7 @@ pub mod test { // `sled_ensure_datasets`. // // Verify that it has created the datasets we expect to exist. - verify_blueprint(&blueprint); + verify_blueprint(&blueprint, &input); let mut builder = BlueprintBuilder::new_based_on( &logctx.log, @@ -3276,7 +3279,7 @@ pub mod test { assert_eq!(r, EnsureMultiple::NotNeeded); let blueprint = builder.build(BlueprintSource::Test); - verify_blueprint(&blueprint); + verify_blueprint(&blueprint, &input); let mut builder = BlueprintBuilder::new_based_on( &logctx.log, @@ -3294,7 +3297,7 @@ pub mod test { assert_eq!(r, EnsureMultiple::NotNeeded); let blueprint = builder.build(BlueprintSource::Test); - verify_blueprint(&blueprint); + verify_blueprint(&blueprint, &input); // Find the datasets we've expunged in the blueprint let expunged_datasets = blueprint @@ -3636,7 +3639,7 @@ pub mod test { builder.sled_ensure_zone_datasets(target_sled_id).unwrap(); let blueprint = builder.build(BlueprintSource::Test); - verify_blueprint(&blueprint); + verify_blueprint(&blueprint, &input); assert_eq!( blueprint .all_omicron_zones(BlueprintZoneDisposition::is_in_service) diff --git a/nexus/reconfigurator/planning/src/planner.rs b/nexus/reconfigurator/planning/src/planner.rs index 05a93109323..661593a59d1 100644 --- a/nexus/reconfigurator/planning/src/planner.rs +++ b/nexus/reconfigurator/planning/src/planner.rs @@ -2560,7 +2560,7 @@ pub(crate) mod test { .expect("created planner"); let child_blueprint = planner.plan().expect("planning should have succeded"); - verify_blueprint(&child_blueprint); + verify_blueprint(&child_blueprint, &input); let summary = child_blueprint.diff_since_blueprint(&blueprint); eprintln!( "diff between blueprints (expected no changes):\n{}", @@ -2584,7 +2584,7 @@ pub(crate) mod test { rng.next_system_rng(), ) .build(); - verify_blueprint(&blueprint1); + verify_blueprint(&blueprint1, &example.input); let input = example .system @@ -2624,7 +2624,7 @@ pub(crate) mod test { assert_eq!(summary.total_datasets_added(), 0); assert_eq!(summary.total_datasets_removed(), 0); assert_eq!(summary.total_datasets_modified(), 0); - verify_blueprint(&blueprint2); + verify_blueprint(&blueprint2, &input); // Now add a new sled. let mut sled_id_rng = rng.next_sled_id_rng(); @@ -2677,7 +2677,7 @@ pub(crate) mod test { )); assert_eq!(summary.diff.sleds.removed.len(), 0); assert_eq!(summary.diff.sleds.modified().count(), 0); - verify_blueprint(&blueprint3); + verify_blueprint(&blueprint3, &input); // Check that with no change in inventory, the planner makes no changes. // It needs to wait for inventory to reflect the new NTP zone before @@ -2698,7 +2698,7 @@ pub(crate) mod test { assert_eq!(summary.diff.sleds.added.len(), 0); assert_eq!(summary.diff.sleds.removed.len(), 0); assert_eq!(summary.diff.sleds.modified().count(), 0); - verify_blueprint(&blueprint4); + verify_blueprint(&blueprint4, &input); // Now update the inventory to have the requested NTP zone. // @@ -2760,7 +2760,7 @@ pub(crate) mod test { panic!("unexpectedly added a non-Crucible zone: {zone:?}"); } } - verify_blueprint(&blueprint5); + verify_blueprint(&blueprint5, &input); // Check that there are no more steps. assert_planning_makes_no_changes( @@ -4922,7 +4922,7 @@ pub(crate) mod test { let (example, blueprint1) = ExampleSystemBuilder::new(&logctx.log, TEST_NAME).build(); let mut collection = example.collection; - verify_blueprint(&blueprint1); + verify_blueprint(&blueprint1, &example.input); // We shouldn't have a clickhouse cluster config, as we don't have a // clickhouse policy set yet @@ -6214,7 +6214,7 @@ pub(crate) mod test { .with_target_release_0_0_1() .expect("set target release to 0.0.1") .build(); - verify_blueprint(&blueprint1); + verify_blueprint(&blueprint1, &example.input); // We should start with nothing to do. assert_planning_makes_no_changes( @@ -6632,7 +6632,7 @@ pub(crate) mod test { .with_target_release_0_0_1() .expect("set target release to 0.0.1") .build(); - verify_blueprint(&blueprint); + verify_blueprint(&blueprint, &example.input); // Update the example system and blueprint, as a part of test set-up. // @@ -6936,7 +6936,7 @@ pub(crate) mod test { .with_target_release_0_0_1() .expect("set target release to 0.0.1") .build(); - verify_blueprint(&blueprint); + verify_blueprint(&blueprint, &example.input); // The example system creates three internal NTP zones, and zero // boundary NTP zones. This is a little arbitrary, but we're checking it @@ -7097,7 +7097,7 @@ pub(crate) mod test { ) .expect("can't create planner"); let new_blueprint = planner.plan().expect("planning succeeded"); - verify_blueprint(&new_blueprint); + verify_blueprint(&new_blueprint, &example.input); { let summary = new_blueprint.diff_since_blueprint(&blueprint); assert_eq!(summary.total_zones_added(), 0); @@ -7555,7 +7555,7 @@ pub(crate) mod test { .with_target_release_0_0_1() .expect("set target release to 0.0.1") .build(); - verify_blueprint(&blueprint); + verify_blueprint(&blueprint, &example.input); // All zones should be sourced from the initial TUF repo by default. assert!( @@ -7746,7 +7746,7 @@ pub(crate) mod test { } blueprint = new_blueprint; update_collection_from_blueprint(&mut example, &blueprint); - verify_blueprint(&blueprint); + verify_blueprint(&blueprint, &example.input); // Next blueprint: Add an (updated) internal DNS zone back @@ -7772,7 +7772,7 @@ pub(crate) mod test { } blueprint = new_blueprint; update_collection_from_blueprint(&mut example, &blueprint); - verify_blueprint(&blueprint); + verify_blueprint(&blueprint, &example.input); assert_eq!( blueprint @@ -7832,7 +7832,7 @@ pub(crate) mod test { .with_target_release_0_0_1() .expect("set target release to 0.0.1") .build(); - verify_blueprint(&blueprint1); + verify_blueprint(&blueprint1, &example.input); // All zones should be sourced from the 0.0.1 repo by default. assert!( diff --git a/nexus/types/src/deployment/network_resources.rs b/nexus/types/src/deployment/network_resources.rs index 2e0cbccd82c..d1c37089943 100644 --- a/nexus/types/src/deployment/network_resources.rs +++ b/nexus/types/src/deployment/network_resources.rs @@ -292,7 +292,9 @@ pub struct OmicronZoneExternalSnatIp { /// /// This is a slimmer `nexus_db_model::ServiceNetworkInterface` that only stores /// the fields necessary for blueprint planning. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, +)] pub struct OmicronZoneNic { pub id: VnicUuid, pub mac: MacAddr, @@ -337,7 +339,9 @@ impl TriHashItem for OmicronZoneExternalIpEntry { /// A pair of an Omicron zone ID and a network interface. /// /// Part of [`OmicronZoneNetworkResources`]. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[derive( + Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, +)] pub struct OmicronZoneNicEntry { pub zone_id: OmicronZoneUuid, pub nic: OmicronZoneNic, diff --git a/sled-agent/src/rack_setup/service.rs b/sled-agent/src/rack_setup/service.rs index 5506159553e..0c56fccb204 100644 --- a/sled-agent/src/rack_setup/service.rs +++ b/sled-agent/src/rack_setup/service.rs @@ -1994,8 +1994,8 @@ mod test { ) .expect("built blueprint"); - let report = - Blippy::new(&blueprint).into_report(BlippyReportSortKey::Kind); + let report = Blippy::new_blueprint_only(&blueprint) + .into_report(BlippyReportSortKey::Kind); if !report.notes().is_empty() { eprintln!("{}", report.display());