diff --git a/Cargo.lock b/Cargo.lock index 63dc1cc735..e1d684da52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4937,6 +4937,7 @@ dependencies = [ "sled-agent-client", "steno", "strum 0.26.1", + "tabled", "thiserror", "uuid 1.7.0", ] diff --git a/clients/sled-agent-client/src/lib.rs b/clients/sled-agent-client/src/lib.rs index 0426982d3e..2901226d16 100644 --- a/clients/sled-agent-client/src/lib.rs +++ b/clients/sled-agent-client/src/lib.rs @@ -8,6 +8,7 @@ use anyhow::Context; use async_trait::async_trait; use omicron_common::api::internal::shared::NetworkInterface; use std::convert::TryFrom; +use std::fmt; use std::hash::Hash; use std::net::IpAddr; use std::net::SocketAddr; @@ -56,25 +57,65 @@ impl Eq for types::OmicronZoneConfig {} impl Eq for types::OmicronZoneType {} impl Eq for types::OmicronZoneDataset {} +/// Like [`types::OmicronZoneType`], but without any associated data. +/// +/// We have a few enums of this form floating around. This particular one is +/// meant to correspond exactly 1:1 with `OmicronZoneType`. +/// +/// The [`fmt::Display`] impl for this type is a human-readable label, meant +/// for testing and reporting. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum ZoneKind { + BoundaryNtp, + Clickhouse, + ClickhouseKeeper, + CockroachDb, + Crucible, + CruciblePantry, + ExternalDns, + InternalDns, + InternalNtp, + Nexus, + Oximeter, +} + +impl fmt::Display for ZoneKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ZoneKind::BoundaryNtp => write!(f, "boundary_ntp"), + ZoneKind::Clickhouse => write!(f, "clickhouse"), + ZoneKind::ClickhouseKeeper => write!(f, "clickhouse_keeper"), + ZoneKind::CockroachDb => write!(f, "cockroach_db"), + ZoneKind::Crucible => write!(f, "crucible"), + ZoneKind::CruciblePantry => write!(f, "crucible_pantry"), + ZoneKind::ExternalDns => write!(f, "external_dns"), + ZoneKind::InternalDns => write!(f, "internal_dns"), + ZoneKind::InternalNtp => write!(f, "internal_ntp"), + ZoneKind::Nexus => write!(f, "nexus"), + ZoneKind::Oximeter => write!(f, "oximeter"), + } + } +} + impl types::OmicronZoneType { - /// Human-readable label describing what kind of zone this is - /// - /// This is just use for testing and reporting. - pub fn label(&self) -> impl std::fmt::Display { + /// Returns the [`ZoneKind`] corresponding to this variant. + pub fn kind(&self) -> ZoneKind { match self { - types::OmicronZoneType::BoundaryNtp { .. } => "boundary_ntp", - types::OmicronZoneType::Clickhouse { .. } => "clickhouse", + types::OmicronZoneType::BoundaryNtp { .. } => ZoneKind::BoundaryNtp, + types::OmicronZoneType::Clickhouse { .. } => ZoneKind::Clickhouse, types::OmicronZoneType::ClickhouseKeeper { .. } => { - "clickhouse_keeper" + ZoneKind::ClickhouseKeeper + } + types::OmicronZoneType::CockroachDb { .. } => ZoneKind::CockroachDb, + types::OmicronZoneType::Crucible { .. } => ZoneKind::Crucible, + types::OmicronZoneType::CruciblePantry { .. } => { + ZoneKind::CruciblePantry } - types::OmicronZoneType::CockroachDb { .. } => "cockroach_db", - types::OmicronZoneType::Crucible { .. } => "crucible", - types::OmicronZoneType::CruciblePantry { .. } => "crucible_pantry", - types::OmicronZoneType::ExternalDns { .. } => "external_dns", - types::OmicronZoneType::InternalDns { .. } => "internal_dns", - types::OmicronZoneType::InternalNtp { .. } => "internal_ntp", - types::OmicronZoneType::Nexus { .. } => "nexus", - types::OmicronZoneType::Oximeter { .. } => "oximeter", + types::OmicronZoneType::ExternalDns { .. } => ZoneKind::ExternalDns, + types::OmicronZoneType::InternalDns { .. } => ZoneKind::InternalDns, + types::OmicronZoneType::InternalNtp { .. } => ZoneKind::InternalNtp, + types::OmicronZoneType::Nexus { .. } => ZoneKind::Nexus, + types::OmicronZoneType::Oximeter { .. } => ZoneKind::Oximeter, } } diff --git a/dev-tools/omdb/src/bin/omdb/db.rs b/dev-tools/omdb/src/bin/omdb/db.rs index 855bbe063b..e1e71ff3d1 100644 --- a/dev-tools/omdb/src/bin/omdb/db.rs +++ b/dev-tools/omdb/src/bin/omdb/db.rs @@ -3244,7 +3244,7 @@ fn inv_collection_print_sleds(collection: &Collection) { println!(" ZONES FOUND"); for z in &zones.zones.zones { - println!(" zone {} (type {})", z.id, z.zone_type.label()); + println!(" zone {} (type {})", z.id, z.zone_type.kind()); } } else { println!(" warning: no zone information found"); diff --git a/dev-tools/omdb/src/bin/omdb/nexus.rs b/dev-tools/omdb/src/bin/omdb/nexus.rs index 26f2e07a41..d3d539cb2c 100644 --- a/dev-tools/omdb/src/bin/omdb/nexus.rs +++ b/dev-tools/omdb/src/bin/omdb/nexus.rs @@ -972,7 +972,8 @@ async fn cmd_nexus_blueprints_diff( let b2 = client.blueprint_view(&args.blueprint2_id).await.with_context( || format!("fetching blueprint {}", args.blueprint2_id), )?; - println!("{}", b1.diff_sleds(&b2).display()); + let diff = b2.diff_since_blueprint(&b1).context("diffing blueprints")?; + println!("{}", diff.display()); Ok(()) } diff --git a/dev-tools/reconfigurator-cli/src/main.rs b/dev-tools/reconfigurator-cli/src/main.rs index 8ba71d9819..358873db44 100644 --- a/dev-tools/reconfigurator-cli/src/main.rs +++ b/dev-tools/reconfigurator-cli/src/main.rs @@ -669,8 +669,10 @@ fn cmd_blueprint_diff( .get(&blueprint2_id) .ok_or_else(|| anyhow!("no such blueprint: {}", blueprint2_id))?; - let sled_diff = blueprint1.diff_sleds(&blueprint2).display().to_string(); - swriteln!(rv, "{}", sled_diff); + let sled_diff = blueprint2 + .diff_since_blueprint(&blueprint1) + .context("failed to diff blueprints")?; + swriteln!(rv, "{}", sled_diff.display()); // Diff'ing DNS is a little trickier. First, compute what DNS should be for // each blueprint. To do that we need to construct a list of sleds suitable @@ -795,7 +797,9 @@ fn cmd_blueprint_diff_inventory( .get(&blueprint_id) .ok_or_else(|| anyhow!("no such blueprint: {}", blueprint_id))?; - let diff = blueprint.diff_sleds_from_collection(&collection); + let diff = blueprint + .diff_since_collection(&collection) + .context("failed to diff blueprint from inventory collection")?; Ok(Some(diff.display().to_string())) } diff --git a/nexus/db-queries/src/db/datastore/deployment.rs b/nexus/db-queries/src/db/datastore/deployment.rs index 02645ca4f6..8f6b9abf58 100644 --- a/nexus/db-queries/src/db/datastore/deployment.rs +++ b/nexus/db-queries/src/db/datastore/deployment.rs @@ -486,6 +486,11 @@ impl DataStore { } } + // Sort all zones to match what blueprint builders do. + for (_, zones_config) in blueprint_zones.iter_mut() { + zones_config.sort(); + } + bail_unless!( omicron_zone_nics.is_empty(), "found extra Omicron zone NICs: {:?}", @@ -1185,6 +1190,7 @@ mod tests { use omicron_common::address::Ipv6Subnet; use omicron_common::api::external::Generation; use omicron_test_utils::dev; + use pretty_assertions::assert_eq; use rand::thread_rng; use rand::Rng; use std::mem; @@ -1515,7 +1521,10 @@ mod tests { .blueprint_read(&opctx, &authz_blueprint2) .await .expect("failed to read collection back"); - println!("diff: {}", blueprint2.diff_sleds(&blueprint_read).display()); + let diff = blueprint_read + .diff_since_blueprint(&blueprint2) + .expect("failed to diff blueprints"); + println!("diff: {}", diff.display()); assert_eq!(blueprint2, blueprint_read); assert_eq!(blueprint2.internal_dns_version, new_internal_dns_version); assert_eq!(blueprint2.external_dns_version, new_external_dns_version); diff --git a/nexus/inventory/src/collector.rs b/nexus/inventory/src/collector.rs index ad5ae7d024..7dbffc396c 100644 --- a/nexus/inventory/src/collector.rs +++ b/nexus/inventory/src/collector.rs @@ -490,7 +490,7 @@ mod test { &mut s, " zone {} type {}\n", zone.id, - zone.zone_type.label(), + zone.zone_type.kind(), ) .unwrap(); } diff --git a/nexus/reconfigurator/execution/src/dns.rs b/nexus/reconfigurator/execution/src/dns.rs index 782e673a17..fc95414103 100644 --- a/nexus/reconfigurator/execution/src/dns.rs +++ b/nexus/reconfigurator/execution/src/dns.rs @@ -281,7 +281,7 @@ pub fn blueprint_internal_dns_config( let context = || { format!( "parsing {} zone with id {}", - zone.config.zone_type.label(), + zone.config.zone_type.kind(), zone.config.id ) }; diff --git a/nexus/reconfigurator/planning/src/blueprint_builder.rs b/nexus/reconfigurator/planning/src/blueprint_builder.rs index ab40f3bbb7..dc0f1e501c 100644 --- a/nexus/reconfigurator/planning/src/blueprint_builder.rs +++ b/nexus/reconfigurator/planning/src/blueprint_builder.rs @@ -860,6 +860,7 @@ pub mod test { use crate::example::example; use crate::example::ExampleSystem; use crate::system::SledBuilder; + use expectorate::assert_contents; use omicron_common::address::IpRange; use omicron_test_utils::dev::test_setup_log; use sled_agent_client::types::{OmicronZoneConfig, OmicronZoneType}; @@ -904,14 +905,23 @@ pub mod test { .expect("failed to create initial blueprint"); verify_blueprint(&blueprint_initial); - let diff = blueprint_initial.diff_sleds_from_collection(&collection); + let diff = + blueprint_initial.diff_since_collection(&collection).unwrap(); + // There are some differences with even a no-op diff between a + // collection and a blueprint, such as new data being added to + // blueprints like DNS generation numbers. println!( - "collection -> initial blueprint (expected no changes):\n{}", + "collection -> initial blueprint \ + (expected no non-trivial changes):\n{}", diff.display() ); - assert_eq!(diff.sleds_added().count(), 0); - assert_eq!(diff.sleds_removed().count(), 0); - assert_eq!(diff.sleds_changed().count(), 0); + assert_contents( + "tests/output/blueprint_builder_initial_diff.txt", + &diff.display().to_string(), + ); + assert_eq!(diff.sleds_added().len(), 0); + assert_eq!(diff.sleds_removed().len(), 0); + assert_eq!(diff.sleds_modified().count(), 0); // Test a no-op blueprint. let builder = BlueprintBuilder::new_based_on( @@ -925,14 +935,14 @@ pub mod test { .expect("failed to create builder"); let blueprint = builder.build(); verify_blueprint(&blueprint); - let diff = blueprint_initial.diff_sleds(&blueprint); + let diff = blueprint.diff_since_blueprint(&blueprint_initial).unwrap(); println!( "initial blueprint -> next blueprint (expected no changes):\n{}", diff.display() ); - assert_eq!(diff.sleds_added().count(), 0); - assert_eq!(diff.sleds_removed().count(), 0); - assert_eq!(diff.sleds_changed().count(), 0); + assert_eq!(diff.sleds_added().len(), 0); + assert_eq!(diff.sleds_removed().len(), 0); + assert_eq!(diff.sleds_modified().count(), 0); logctx.cleanup_successful(); } @@ -970,14 +980,14 @@ pub mod test { let blueprint2 = builder.build(); verify_blueprint(&blueprint2); - let diff = blueprint1.diff_sleds(&blueprint2); + let diff = blueprint2.diff_since_blueprint(&blueprint1).unwrap(); println!( "initial blueprint -> next blueprint (expected no changes):\n{}", diff.display() ); - assert_eq!(diff.sleds_added().count(), 0); - assert_eq!(diff.sleds_removed().count(), 0); - assert_eq!(diff.sleds_changed().count(), 0); + assert_eq!(diff.sleds_added().len(), 0); + assert_eq!(diff.sleds_removed().len(), 0); + assert_eq!(diff.sleds_modified().count(), 0); // The next step is adding these zones to a new sled. let new_sled_id = example.sled_rng.next(); @@ -1003,12 +1013,12 @@ pub mod test { let blueprint3 = builder.build(); verify_blueprint(&blueprint3); - let diff = blueprint2.diff_sleds(&blueprint3); + let diff = blueprint3.diff_since_blueprint(&blueprint2).unwrap(); println!("expecting new NTP and Crucible zones:\n{}", diff.display()); // No sleds were changed or removed. - assert_eq!(diff.sleds_changed().count(), 0); - assert_eq!(diff.sleds_removed().count(), 0); + assert_eq!(diff.sleds_modified().count(), 0); + assert_eq!(diff.sleds_removed().len(), 0); // One sled was added. let sleds: Vec<_> = diff.sleds_added().collect(); diff --git a/nexus/reconfigurator/planning/src/planner.rs b/nexus/reconfigurator/planning/src/planner.rs index 84360aded9..ce5660e7f6 100644 --- a/nexus/reconfigurator/planning/src/planner.rs +++ b/nexus/reconfigurator/planning/src/planner.rs @@ -338,8 +338,12 @@ mod test { use crate::example::example; use crate::example::ExampleSystem; use crate::system::SledBuilder; + use chrono::NaiveDateTime; + use chrono::TimeZone; + use chrono::Utc; use expectorate::assert_contents; use nexus_inventory::now_db_precision; + use nexus_types::deployment::BlueprintZoneDisposition; use nexus_types::deployment::BlueprintZoneFilter; use nexus_types::external_api::views::SledPolicy; use nexus_types::external_api::views::SledProvisionPolicy; @@ -394,11 +398,11 @@ mod test { .plan() .expect("failed to plan"); - let diff = blueprint1.diff_sleds(&blueprint2); + let diff = blueprint2.diff_since_blueprint(&blueprint1).unwrap(); println!("1 -> 2 (expected no changes):\n{}", diff.display()); - assert_eq!(diff.sleds_added().count(), 0); - assert_eq!(diff.sleds_removed().count(), 0); - assert_eq!(diff.sleds_changed().count(), 0); + assert_eq!(diff.sleds_added().len(), 0); + assert_eq!(diff.sleds_removed().len(), 0); + assert_eq!(diff.sleds_modified().count(), 0); verify_blueprint(&blueprint2); // Now add a new sled. @@ -422,7 +426,7 @@ mod test { .plan() .expect("failed to plan"); - let diff = blueprint2.diff_sleds(&blueprint3); + let diff = blueprint3.diff_since_blueprint(&blueprint2).unwrap(); println!( "2 -> 3 (expect new NTP zone on new sled):\n{}", diff.display() @@ -443,8 +447,8 @@ mod test { sled_zones.zones[0].config.zone_type, OmicronZoneType::InternalNtp { .. } )); - assert_eq!(diff.sleds_removed().count(), 0); - assert_eq!(diff.sleds_changed().count(), 0); + assert_eq!(diff.sleds_removed().len(), 0); + assert_eq!(diff.sleds_modified().count(), 0); verify_blueprint(&blueprint3); // Check that with no change in inventory, the planner makes no changes. @@ -463,11 +467,11 @@ mod test { .with_rng_seed((TEST_NAME, "bp4")) .plan() .expect("failed to plan"); - let diff = blueprint3.diff_sleds(&blueprint4); + let diff = blueprint4.diff_since_blueprint(&blueprint3).unwrap(); println!("3 -> 4 (expected no changes):\n{}", diff.display()); - assert_eq!(diff.sleds_added().count(), 0); - assert_eq!(diff.sleds_removed().count(), 0); - assert_eq!(diff.sleds_changed().count(), 0); + assert_eq!(diff.sleds_added().len(), 0); + assert_eq!(diff.sleds_removed().len(), 0); + assert_eq!(diff.sleds_modified().count(), 0); verify_blueprint(&blueprint4); // Now update the inventory to have the requested NTP zone. @@ -506,15 +510,15 @@ mod test { .plan() .expect("failed to plan"); - let diff = blueprint3.diff_sleds(&blueprint5); + let diff = blueprint5.diff_since_blueprint(&blueprint3).unwrap(); println!("3 -> 5 (expect Crucible zones):\n{}", diff.display()); assert_contents( "tests/output/planner_basic_add_sled_3_5.txt", &diff.display().to_string(), ); - assert_eq!(diff.sleds_added().count(), 0); - assert_eq!(diff.sleds_removed().count(), 0); - let sleds = diff.sleds_changed().collect::>(); + assert_eq!(diff.sleds_added().len(), 0); + assert_eq!(diff.sleds_removed().len(), 0); + let sleds = diff.sleds_modified().collect::>(); assert_eq!(sleds.len(), 1); let (sled_id, sled_changes) = &sleds[0]; assert_eq!( @@ -522,8 +526,8 @@ mod test { sled_changes.generation_before.next() ); assert_eq!(*sled_id, new_sled_id); - assert_eq!(sled_changes.zones_removed().count(), 0); - assert_eq!(sled_changes.zones_changed().count(), 0); + assert_eq!(sled_changes.zones_removed().len(), 0); + assert_eq!(sled_changes.zones_modified().count(), 0); let zones = sled_changes.zones_added().collect::>(); assert_eq!(zones.len(), 10); for zone in &zones { @@ -548,11 +552,11 @@ mod test { .plan() .expect("failed to plan"); - let diff = blueprint5.diff_sleds(&blueprint6); + let diff = blueprint6.diff_since_blueprint(&blueprint5).unwrap(); println!("5 -> 6 (expect no changes):\n{}", diff.display()); - assert_eq!(diff.sleds_added().count(), 0); - assert_eq!(diff.sleds_removed().count(), 0); - assert_eq!(diff.sleds_changed().count(), 0); + assert_eq!(diff.sleds_added().len(), 0); + assert_eq!(diff.sleds_removed().len(), 0); + assert_eq!(diff.sleds_modified().count(), 0); verify_blueprint(&blueprint6); logctx.cleanup_successful(); @@ -624,7 +628,7 @@ mod test { internal_dns_version, external_dns_version, &policy, - "add more Nexus", + "test_blueprint2", &collection, ) .expect("failed to create planner") @@ -632,16 +636,16 @@ mod test { .plan() .expect("failed to plan"); - let diff = blueprint1.diff_sleds(&blueprint2); + let diff = blueprint2.diff_since_blueprint(&blueprint1).unwrap(); println!("1 -> 2 (added additional Nexus zones):\n{}", diff.display()); - assert_eq!(diff.sleds_added().count(), 0); - assert_eq!(diff.sleds_removed().count(), 0); - let mut sleds = diff.sleds_changed().collect::>(); + assert_eq!(diff.sleds_added().len(), 0); + assert_eq!(diff.sleds_removed().len(), 0); + let mut sleds = diff.sleds_modified().collect::>(); assert_eq!(sleds.len(), 1); let (changed_sled_id, sled_changes) = sleds.pop().unwrap(); assert_eq!(changed_sled_id, sled_id); - assert_eq!(sled_changes.zones_removed().count(), 0); - assert_eq!(sled_changes.zones_changed().count(), 0); + assert_eq!(sled_changes.zones_removed().len(), 0); + assert_eq!(sled_changes.zones_modified().count(), 0); let zones = sled_changes.zones_added().collect::>(); assert_eq!(zones.len(), policy.target_nexus_zone_count - 1); for zone in &zones { @@ -698,7 +702,7 @@ mod test { Generation::new(), Generation::new(), &policy, - "add more Nexus", + "test_blueprint2", &collection, ) .expect("failed to create planner") @@ -706,11 +710,11 @@ mod test { .plan() .expect("failed to plan"); - let diff = blueprint1.diff_sleds(&blueprint2); + let diff = blueprint2.diff_since_blueprint(&blueprint1).unwrap(); println!("1 -> 2 (added additional Nexus zones):\n{}", diff.display()); - assert_eq!(diff.sleds_added().count(), 0); - assert_eq!(diff.sleds_removed().count(), 0); - let sleds = diff.sleds_changed().collect::>(); + assert_eq!(diff.sleds_added().len(), 0); + assert_eq!(diff.sleds_removed().len(), 0); + let sleds = diff.sleds_modified().collect::>(); // All 3 sleds should get additional Nexus zones. We expect a total of // 11 new Nexus zones, which should be spread evenly across the three @@ -718,8 +722,8 @@ mod test { assert_eq!(sleds.len(), 3); let mut total_new_nexus_zones = 0; for (sled_id, sled_changes) in sleds { - assert_eq!(sled_changes.zones_removed().count(), 0); - assert_eq!(sled_changes.zones_changed().count(), 0); + assert_eq!(sled_changes.zones_removed().len(), 0); + assert_eq!(sled_changes.zones_modified().count(), 0); let zones = sled_changes.zones_added().collect::>(); match zones.len() { n @ (3 | 4) => { @@ -814,13 +818,13 @@ mod test { // When the planner gets smarter about removing zones from expunged // and/or removed sleds, we'll have to adjust this number. policy.target_nexus_zone_count = 16; - let blueprint2 = Planner::new_based_on( + let mut blueprint2 = Planner::new_based_on( logctx.log.clone(), &blueprint1, Generation::new(), Generation::new(), &policy, - "add more Nexus", + "test_blueprint2", &collection, ) .expect("failed to create planner") @@ -828,15 +832,24 @@ mod test { .plan() .expect("failed to plan"); - let diff = blueprint1.diff_sleds(&blueprint2); + // Define a time_created for consistent output across runs. + blueprint2.time_created = + Utc.from_utc_datetime(&NaiveDateTime::UNIX_EPOCH); + + assert_contents( + "tests/output/planner_nonprovisionable_bp2.txt", + &blueprint2.display().to_string(), + ); + + let diff = blueprint2.diff_since_blueprint(&blueprint1).unwrap(); println!("1 -> 2 (added additional Nexus zones):\n{}", diff.display()); assert_contents( "tests/output/planner_nonprovisionable_1_2.txt", &diff.display().to_string(), ); - assert_eq!(diff.sleds_added().count(), 0); - assert_eq!(diff.sleds_removed().count(), 0); - let sleds = diff.sleds_changed().collect::>(); + assert_eq!(diff.sleds_added().len(), 0); + assert_eq!(diff.sleds_removed().len(), 0); + let sleds = diff.sleds_modified().collect::>(); // Only 2 of the 3 sleds should get additional Nexus zones. We expect a // total of 12 new Nexus zones, which should be spread evenly across the @@ -848,8 +861,8 @@ mod test { assert!(sled_id != nonprovisionable_sled_id); assert!(sled_id != expunged_sled_id); assert!(sled_id != decommissioned_sled_id); - assert_eq!(sled_changes.zones_removed().count(), 0); - assert_eq!(sled_changes.zones_changed().count(), 0); + assert_eq!(sled_changes.zones_removed().len(), 0); + assert_eq!(sled_changes.zones_modified().count(), 0); let zones = sled_changes.zones_added().collect::>(); match zones.len() { n @ (5 | 6) => { @@ -868,6 +881,90 @@ mod test { } assert_eq!(total_new_nexus_zones, 11); + // --- + + // Also poke at some of the config by hand; we'll use this to test out + // diff output. This isn't a real blueprint, just one that we're + // creating to test diff output. + // + // Some of the things we're testing here: + // + // * modifying zones + // * removing zones + // * removing sleds + // * for modified sleds' zone config generation, both a bump and the + // generation staying the same (the latter should produce a warning) + let mut blueprint2a = blueprint2.clone(); + + enum NextCrucibleMutate { + Modify, + Remove, + Done, + } + let mut next = NextCrucibleMutate::Modify; + + // Leave the non-provisionable sled's generation alone. + let zones = &mut blueprint2a + .blueprint_zones + .get_mut(&nonprovisionable_sled_id) + .unwrap() + .zones; + + zones.retain_mut(|zone| { + if let OmicronZoneType::Nexus { internal_address, .. } = + &mut zone.config.zone_type + { + // Change one of these params to ensure that the diff output + // makes sense. + *internal_address = format!("{internal_address}foo"); + true + } else if let OmicronZoneType::Crucible { .. } = + zone.config.zone_type + { + match next { + NextCrucibleMutate::Modify => { + zone.disposition = BlueprintZoneDisposition::Quiesced; + next = NextCrucibleMutate::Remove; + true + } + NextCrucibleMutate::Remove => { + next = NextCrucibleMutate::Done; + false + } + NextCrucibleMutate::Done => true, + } + } else if let OmicronZoneType::InternalNtp { .. } = + &mut zone.config.zone_type + { + // Change the underlay IP. + let mut segments = zone.config.underlay_address.segments(); + segments[0] += 1; + zone.config.underlay_address = segments.into(); + true + } else { + true + } + }); + + let expunged_zones = + blueprint2a.blueprint_zones.get_mut(&expunged_sled_id).unwrap(); + expunged_zones.zones.clear(); + expunged_zones.generation = expunged_zones.generation.next(); + + blueprint2a.blueprint_zones.remove(&decommissioned_sled_id); + + blueprint2a.external_dns_version = + blueprint2a.external_dns_version.next(); + + let diff = blueprint2a.diff_since_blueprint(&blueprint2).unwrap(); + println!("2 -> 2a (manually modified zones):\n{}", diff.display()); + assert_contents( + "tests/output/planner_nonprovisionable_2_2a.txt", + &diff.display().to_string(), + ); + + // --- + logctx.cleanup_successful(); } } diff --git a/nexus/reconfigurator/planning/tests/output/blueprint_builder_initial_diff.txt b/nexus/reconfigurator/planning/tests/output/blueprint_builder_initial_diff.txt new file mode 100644 index 0000000000..7323008ad1 --- /dev/null +++ b/nexus/reconfigurator/planning/tests/output/blueprint_builder_initial_diff.txt @@ -0,0 +1,54 @@ +from: collection 094d362b-7d79-49e7-a244-134276cca8fe +to: blueprint 9d2c007b-46f1-4ff2-8b4c-8a5767030f76 + + ------------------------------------------------------------------------------------------------------ + zone type zone ID disposition underlay IP status + ------------------------------------------------------------------------------------------------------ + + UNCHANGED SLEDS: + + sled 08c7046b-c9c4-4368-881f-19a72df22143: zones at generation 2 + crucible 44afce85-3377-4b20-a398-517c1579df4d in service fd00:1122:3344:103::23 + crucible 4644ea0c-0ec3-41be-a356-660308e1c3fc in service fd00:1122:3344:103::2c + crucible 55f4d117-0b9d-4256-a2c0-f46d3ed5fff9 in service fd00:1122:3344:103::25 + crucible 5c6a4628-8831-483b-995f-79b9126c4d04 in service fd00:1122:3344:103::28 + crucible 6a01210c-45ed-41a5-9230-8e05ecf5dd8f in service fd00:1122:3344:103::29 + crucible 7004cab9-dfc0-43ba-92d3-58d4ced66025 in service fd00:1122:3344:103::24 + crucible 79552859-fbd3-43bb-a9d3-6baba25558f8 in service fd00:1122:3344:103::26 + crucible 90696819-9b53-485a-9c65-ca63602e843e in service fd00:1122:3344:103::27 + crucible c99525b3-3680-4df6-9214-2ee3e1020e8b in service fd00:1122:3344:103::2a + crucible f42959d3-9eef-4e3b-b404-6177ce3ec7a1 in service fd00:1122:3344:103::2b + internal_ntp c81c9d4a-36d7-4796-9151-f564d3735152 in service fd00:1122:3344:103::21 + nexus b2573120-9c91-4ed7-8b4f-a7bfe8dbc807 in service fd00:1122:3344:103::22 + + sled 84ac367e-9b03-4e9d-a846-df1a08deee6c: zones at generation 2 + crucible 0faa9350-2c02-47c7-a0a6-9f4afd69152c in service fd00:1122:3344:101::2c + crucible 5b44003e-1a3d-4152-b606-872c72efce0e in service fd00:1122:3344:101::25 + crucible 943fea7a-9458-4935-9dc7-01ee5cfe5a02 in service fd00:1122:3344:101::29 + crucible 95c3b6d1-2592-4252-b5c1-5d0faf3ce9c9 in service fd00:1122:3344:101::24 + crucible a5a0b7a9-37c9-4dbd-8393-ec7748ada3b0 in service fd00:1122:3344:101::2b + crucible a9a6a974-8953-4783-b815-da46884f2c02 in service fd00:1122:3344:101::23 + crucible aa25add8-60b0-4ace-ac60-15adcdd32d50 in service fd00:1122:3344:101::2a + crucible b6f2dd1e-7f98-4a68-9df2-b33c69d1f7ea in service fd00:1122:3344:101::27 + crucible dc22d470-dc46-436b-9750-25c8d7d369e2 in service fd00:1122:3344:101::26 + crucible f7e434f9-6d4a-476b-a9e2-48d6ee28a08e in service fd00:1122:3344:101::28 + internal_ntp 38b047ea-e3de-4859-b8e0-70cac5871446 in service fd00:1122:3344:101::21 + nexus fb36b9dc-273a-4bc3-aaa9-19ee4d0ef552 in service fd00:1122:3344:101::22 + + sled be7f4375-2a6b-457f-b1a4-3074a715e5fe: zones at generation 2 + crucible 248db330-56e6-4c7e-b5ff-9cd6cbcb210a in service fd00:1122:3344:102::2c + crucible 353b0aff-4c71-4fae-a6bd-adcb1d2a1a1d in service fd00:1122:3344:102::29 + crucible 4330134c-41b9-4097-aa0b-3eaefa06d473 in service fd00:1122:3344:102::24 + crucible 65d03287-e43f-45f4-902e-0a5e4638f31a in service fd00:1122:3344:102::25 + crucible 6a5901b1-f9d7-425c-8ecb-a786c900f217 in service fd00:1122:3344:102::27 + crucible 9b722fea-a186-4bc3-bc37-ce7f6de6a796 in service fd00:1122:3344:102::23 + crucible b3583b5f-4a62-4471-9be7-41e61578de4c in service fd00:1122:3344:102::2a + crucible bac92034-b9e6-4e8b-9ffb-dbba9caec88d in service fd00:1122:3344:102::28 + crucible d9653001-f671-4905-a410-6a7abc358318 in service fd00:1122:3344:102::2b + crucible edaca77e-5806-446a-b00c-125962cd551d in service fd00:1122:3344:102::26 + internal_ntp aac3ab51-9e2b-4605-9bf6-e3eb3681c2b5 in service fd00:1122:3344:102::21 + nexus 29278a22-1ba1-4117-bfdb-39fcb9ae7fd1 in service fd00:1122:3344:102::22 + + METADATA: ++ internal DNS version: (not present in collection) -> 1 ++ external DNS version: (not present in collection) -> 1 diff --git a/nexus/reconfigurator/planning/tests/output/planner_basic_add_sled_2_3.txt b/nexus/reconfigurator/planning/tests/output/planner_basic_add_sled_2_3.txt index 9f7cab737f..3aad697aa0 100644 --- a/nexus/reconfigurator/planning/tests/output/planner_basic_add_sled_2_3.txt +++ b/nexus/reconfigurator/planning/tests/output/planner_basic_add_sled_2_3.txt @@ -1,48 +1,59 @@ -diff blueprint 979ef428-0bdd-4622-8a72-0719e942b415 blueprint 4171ad05-89dd-474b-846b-b007e4346366 ---- blueprint 979ef428-0bdd-4622-8a72-0719e942b415 -+++ blueprint 4171ad05-89dd-474b-846b-b007e4346366 - sled 41f45d9f-766e-4ca6-a881-61ee45c80f57 - zone config generation 2 - 267ed614-92af-4b9d-bdba-c2881c2e43a2 in service internal_ntp [underlay IP fd00:1122:3344:103::21] (unchanged) - 322ee9f1-8903-4542-a0a8-a54cefabdeca in service crucible [underlay IP fd00:1122:3344:103::24] (unchanged) - 4ab1650f-32c5-447f-939d-64b8103a7645 in service crucible [underlay IP fd00:1122:3344:103::2a] (unchanged) - 64aa65f8-1ccb-4cd6-9953-027aebdac8ff in service crucible [underlay IP fd00:1122:3344:103::27] (unchanged) - 6e811d86-8aa7-4660-935b-84b4b7721b10 in service crucible [underlay IP fd00:1122:3344:103::2b] (unchanged) - 747d2426-68bf-4c22-8806-41d290b5d5f5 in service crucible [underlay IP fd00:1122:3344:103::25] (unchanged) - 7fbd2c38-5dc3-48c4-b061-558a2041d70f in service crucible [underlay IP fd00:1122:3344:103::2c] (unchanged) - 8e9e923e-62b1-4cbc-9f59-d6397e338b6b in service crucible [underlay IP fd00:1122:3344:103::29] (unchanged) - b14d5478-1a0e-4b90-b526-36b06339dfc4 in service crucible [underlay IP fd00:1122:3344:103::28] (unchanged) - b40f7c7b-526c-46c8-ae33-67280c280eb7 in service crucible [underlay IP fd00:1122:3344:103::23] (unchanged) - be97b92b-38d6-422a-8c76-d37060f75bd2 in service crucible [underlay IP fd00:1122:3344:103::26] (unchanged) - cc816cfe-3869-4dde-b596-397d41198628 in service nexus [underlay IP fd00:1122:3344:103::22] (unchanged) - sled 43677374-8d2f-4deb-8a41-eeea506db8e0 - zone config generation 2 - 02acbe6a-1c88-47e3-94c3-94084cbde098 in service crucible [underlay IP fd00:1122:3344:101::27] (unchanged) - 07c3c805-8888-4fe5-9543-3d2479dbe6f3 in service crucible [underlay IP fd00:1122:3344:101::26] (unchanged) - 08c7f8aa-1ea9-469b-8cac-2fdbfc11ebcb in service internal_ntp [underlay IP fd00:1122:3344:101::21] (unchanged) - 10d98a73-ec88-4aff-a7e8-7db6a87880e6 in service crucible [underlay IP fd00:1122:3344:101::24] (unchanged) - 2a455c35-eb3c-4c73-ab6c-d0a706e25316 in service crucible [underlay IP fd00:1122:3344:101::29] (unchanged) - 3eda924f-22a9-4f3e-9a1b-91d1c47601ab in service crucible [underlay IP fd00:1122:3344:101::23] (unchanged) - 587be699-a320-4c79-b320-128d9ecddc0b in service crucible [underlay IP fd00:1122:3344:101::2b] (unchanged) - 6fa06115-4959-4913-8e7b-dd70d7651f07 in service crucible [underlay IP fd00:1122:3344:101::2c] (unchanged) - 8f3a1cc5-9195-4a30-ad02-b804278fe639 in service crucible [underlay IP fd00:1122:3344:101::28] (unchanged) - a1696cd4-588c-484a-b95b-66e824c0ce05 in service crucible [underlay IP fd00:1122:3344:101::25] (unchanged) - a2079cbc-a69e-41a1-b1e0-fbcb972d03f6 in service crucible [underlay IP fd00:1122:3344:101::2a] (unchanged) - c66ab6d5-ff7a-46d1-9fd0-70cefa352d25 in service nexus [underlay IP fd00:1122:3344:101::22] (unchanged) - sled 590e3034-d946-4166-b0e5-2d0034197a07 - zone config generation 2 - 18f8fe40-646e-4962-b17a-20e201f3a6e5 in service crucible [underlay IP fd00:1122:3344:102::2a] (unchanged) - 47199d48-534c-4267-a654-d2d90e64b498 in service internal_ntp [underlay IP fd00:1122:3344:102::21] (unchanged) - 56d5d7cf-db2c-40a3-a775-003241ad4820 in service crucible [underlay IP fd00:1122:3344:102::29] (unchanged) - 6af7f4d6-33b6-4eb3-a146-d8e9e4ae9d66 in service crucible [underlay IP fd00:1122:3344:102::2b] (unchanged) - 704e1fed-f8d6-4cfa-a470-bad27fdc06d1 in service nexus [underlay IP fd00:1122:3344:102::22] (unchanged) - 7a9f60d3-2b66-4547-9b63-7d4f7a8b6382 in service crucible [underlay IP fd00:1122:3344:102::26] (unchanged) - 93f2f40c-5616-4d8d-8519-ec6debdcede0 in service crucible [underlay IP fd00:1122:3344:102::2c] (unchanged) - ab7ba6df-d401-40bd-940e-faf57c57aa2a in service crucible [underlay IP fd00:1122:3344:102::28] (unchanged) - af322036-371f-437c-8c08-7f40f3f1403b in service crucible [underlay IP fd00:1122:3344:102::23] (unchanged) - d637264f-6f40-44c2-8b7e-a179430210d2 in service crucible [underlay IP fd00:1122:3344:102::25] (unchanged) - dce226c9-7373-4bfa-8a94-79dc472857a6 in service crucible [underlay IP fd00:1122:3344:102::27] (unchanged) - edabedf3-839c-488d-ad6f-508ffa864674 in service crucible [underlay IP fd00:1122:3344:102::24] (unchanged) -+ sled b59ec570-2abb-4017-80ce-129d94e7a025 (added) -+ zone config generation 2 -+ 2d73d30e-ca47-46a8-9c12-917d4ab824b6 in service internal_ntp [underlay IP fd00:1122:3344:104::21] (added) +from: blueprint 979ef428-0bdd-4622-8a72-0719e942b415 +to: blueprint 4171ad05-89dd-474b-846b-b007e4346366 + + ------------------------------------------------------------------------------------------------------ + zone type zone ID disposition underlay IP status + ------------------------------------------------------------------------------------------------------ + + UNCHANGED SLEDS: + + sled 41f45d9f-766e-4ca6-a881-61ee45c80f57: zones at generation 2 + crucible 322ee9f1-8903-4542-a0a8-a54cefabdeca in service fd00:1122:3344:103::24 + crucible 4ab1650f-32c5-447f-939d-64b8103a7645 in service fd00:1122:3344:103::2a + crucible 64aa65f8-1ccb-4cd6-9953-027aebdac8ff in service fd00:1122:3344:103::27 + crucible 6e811d86-8aa7-4660-935b-84b4b7721b10 in service fd00:1122:3344:103::2b + crucible 747d2426-68bf-4c22-8806-41d290b5d5f5 in service fd00:1122:3344:103::25 + crucible 7fbd2c38-5dc3-48c4-b061-558a2041d70f in service fd00:1122:3344:103::2c + crucible 8e9e923e-62b1-4cbc-9f59-d6397e338b6b in service fd00:1122:3344:103::29 + crucible b14d5478-1a0e-4b90-b526-36b06339dfc4 in service fd00:1122:3344:103::28 + crucible b40f7c7b-526c-46c8-ae33-67280c280eb7 in service fd00:1122:3344:103::23 + crucible be97b92b-38d6-422a-8c76-d37060f75bd2 in service fd00:1122:3344:103::26 + internal_ntp 267ed614-92af-4b9d-bdba-c2881c2e43a2 in service fd00:1122:3344:103::21 + nexus cc816cfe-3869-4dde-b596-397d41198628 in service fd00:1122:3344:103::22 + + sled 43677374-8d2f-4deb-8a41-eeea506db8e0: zones at generation 2 + crucible 02acbe6a-1c88-47e3-94c3-94084cbde098 in service fd00:1122:3344:101::27 + crucible 07c3c805-8888-4fe5-9543-3d2479dbe6f3 in service fd00:1122:3344:101::26 + crucible 10d98a73-ec88-4aff-a7e8-7db6a87880e6 in service fd00:1122:3344:101::24 + crucible 2a455c35-eb3c-4c73-ab6c-d0a706e25316 in service fd00:1122:3344:101::29 + crucible 3eda924f-22a9-4f3e-9a1b-91d1c47601ab in service fd00:1122:3344:101::23 + crucible 587be699-a320-4c79-b320-128d9ecddc0b in service fd00:1122:3344:101::2b + crucible 6fa06115-4959-4913-8e7b-dd70d7651f07 in service fd00:1122:3344:101::2c + crucible 8f3a1cc5-9195-4a30-ad02-b804278fe639 in service fd00:1122:3344:101::28 + crucible a1696cd4-588c-484a-b95b-66e824c0ce05 in service fd00:1122:3344:101::25 + crucible a2079cbc-a69e-41a1-b1e0-fbcb972d03f6 in service fd00:1122:3344:101::2a + internal_ntp 08c7f8aa-1ea9-469b-8cac-2fdbfc11ebcb in service fd00:1122:3344:101::21 + nexus c66ab6d5-ff7a-46d1-9fd0-70cefa352d25 in service fd00:1122:3344:101::22 + + sled 590e3034-d946-4166-b0e5-2d0034197a07: zones at generation 2 + crucible 18f8fe40-646e-4962-b17a-20e201f3a6e5 in service fd00:1122:3344:102::2a + crucible 56d5d7cf-db2c-40a3-a775-003241ad4820 in service fd00:1122:3344:102::29 + crucible 6af7f4d6-33b6-4eb3-a146-d8e9e4ae9d66 in service fd00:1122:3344:102::2b + crucible 7a9f60d3-2b66-4547-9b63-7d4f7a8b6382 in service fd00:1122:3344:102::26 + crucible 93f2f40c-5616-4d8d-8519-ec6debdcede0 in service fd00:1122:3344:102::2c + crucible ab7ba6df-d401-40bd-940e-faf57c57aa2a in service fd00:1122:3344:102::28 + crucible af322036-371f-437c-8c08-7f40f3f1403b in service fd00:1122:3344:102::23 + crucible d637264f-6f40-44c2-8b7e-a179430210d2 in service fd00:1122:3344:102::25 + crucible dce226c9-7373-4bfa-8a94-79dc472857a6 in service fd00:1122:3344:102::27 + crucible edabedf3-839c-488d-ad6f-508ffa864674 in service fd00:1122:3344:102::24 + internal_ntp 47199d48-534c-4267-a654-d2d90e64b498 in service fd00:1122:3344:102::21 + nexus 704e1fed-f8d6-4cfa-a470-bad27fdc06d1 in service fd00:1122:3344:102::22 + + ADDED SLEDS: + ++ sled b59ec570-2abb-4017-80ce-129d94e7a025: zones at generation 2 ++ internal_ntp 2d73d30e-ca47-46a8-9c12-917d4ab824b6 in service fd00:1122:3344:104::21 added + + METADATA: + internal DNS version: 1 (unchanged) + external DNS version: 1 (unchanged) diff --git a/nexus/reconfigurator/planning/tests/output/planner_basic_add_sled_3_5.txt b/nexus/reconfigurator/planning/tests/output/planner_basic_add_sled_3_5.txt index 9d98daac36..233821412f 100644 --- a/nexus/reconfigurator/planning/tests/output/planner_basic_add_sled_3_5.txt +++ b/nexus/reconfigurator/planning/tests/output/planner_basic_add_sled_3_5.txt @@ -1,59 +1,69 @@ -diff blueprint 4171ad05-89dd-474b-846b-b007e4346366 blueprint f432fcd5-1284-4058-8b4a-9286a3de6163 ---- blueprint 4171ad05-89dd-474b-846b-b007e4346366 -+++ blueprint f432fcd5-1284-4058-8b4a-9286a3de6163 - sled 41f45d9f-766e-4ca6-a881-61ee45c80f57 - zone config generation 2 - 267ed614-92af-4b9d-bdba-c2881c2e43a2 in service internal_ntp [underlay IP fd00:1122:3344:103::21] (unchanged) - 322ee9f1-8903-4542-a0a8-a54cefabdeca in service crucible [underlay IP fd00:1122:3344:103::24] (unchanged) - 4ab1650f-32c5-447f-939d-64b8103a7645 in service crucible [underlay IP fd00:1122:3344:103::2a] (unchanged) - 64aa65f8-1ccb-4cd6-9953-027aebdac8ff in service crucible [underlay IP fd00:1122:3344:103::27] (unchanged) - 6e811d86-8aa7-4660-935b-84b4b7721b10 in service crucible [underlay IP fd00:1122:3344:103::2b] (unchanged) - 747d2426-68bf-4c22-8806-41d290b5d5f5 in service crucible [underlay IP fd00:1122:3344:103::25] (unchanged) - 7fbd2c38-5dc3-48c4-b061-558a2041d70f in service crucible [underlay IP fd00:1122:3344:103::2c] (unchanged) - 8e9e923e-62b1-4cbc-9f59-d6397e338b6b in service crucible [underlay IP fd00:1122:3344:103::29] (unchanged) - b14d5478-1a0e-4b90-b526-36b06339dfc4 in service crucible [underlay IP fd00:1122:3344:103::28] (unchanged) - b40f7c7b-526c-46c8-ae33-67280c280eb7 in service crucible [underlay IP fd00:1122:3344:103::23] (unchanged) - be97b92b-38d6-422a-8c76-d37060f75bd2 in service crucible [underlay IP fd00:1122:3344:103::26] (unchanged) - cc816cfe-3869-4dde-b596-397d41198628 in service nexus [underlay IP fd00:1122:3344:103::22] (unchanged) - sled 43677374-8d2f-4deb-8a41-eeea506db8e0 - zone config generation 2 - 02acbe6a-1c88-47e3-94c3-94084cbde098 in service crucible [underlay IP fd00:1122:3344:101::27] (unchanged) - 07c3c805-8888-4fe5-9543-3d2479dbe6f3 in service crucible [underlay IP fd00:1122:3344:101::26] (unchanged) - 08c7f8aa-1ea9-469b-8cac-2fdbfc11ebcb in service internal_ntp [underlay IP fd00:1122:3344:101::21] (unchanged) - 10d98a73-ec88-4aff-a7e8-7db6a87880e6 in service crucible [underlay IP fd00:1122:3344:101::24] (unchanged) - 2a455c35-eb3c-4c73-ab6c-d0a706e25316 in service crucible [underlay IP fd00:1122:3344:101::29] (unchanged) - 3eda924f-22a9-4f3e-9a1b-91d1c47601ab in service crucible [underlay IP fd00:1122:3344:101::23] (unchanged) - 587be699-a320-4c79-b320-128d9ecddc0b in service crucible [underlay IP fd00:1122:3344:101::2b] (unchanged) - 6fa06115-4959-4913-8e7b-dd70d7651f07 in service crucible [underlay IP fd00:1122:3344:101::2c] (unchanged) - 8f3a1cc5-9195-4a30-ad02-b804278fe639 in service crucible [underlay IP fd00:1122:3344:101::28] (unchanged) - a1696cd4-588c-484a-b95b-66e824c0ce05 in service crucible [underlay IP fd00:1122:3344:101::25] (unchanged) - a2079cbc-a69e-41a1-b1e0-fbcb972d03f6 in service crucible [underlay IP fd00:1122:3344:101::2a] (unchanged) - c66ab6d5-ff7a-46d1-9fd0-70cefa352d25 in service nexus [underlay IP fd00:1122:3344:101::22] (unchanged) - sled 590e3034-d946-4166-b0e5-2d0034197a07 - zone config generation 2 - 18f8fe40-646e-4962-b17a-20e201f3a6e5 in service crucible [underlay IP fd00:1122:3344:102::2a] (unchanged) - 47199d48-534c-4267-a654-d2d90e64b498 in service internal_ntp [underlay IP fd00:1122:3344:102::21] (unchanged) - 56d5d7cf-db2c-40a3-a775-003241ad4820 in service crucible [underlay IP fd00:1122:3344:102::29] (unchanged) - 6af7f4d6-33b6-4eb3-a146-d8e9e4ae9d66 in service crucible [underlay IP fd00:1122:3344:102::2b] (unchanged) - 704e1fed-f8d6-4cfa-a470-bad27fdc06d1 in service nexus [underlay IP fd00:1122:3344:102::22] (unchanged) - 7a9f60d3-2b66-4547-9b63-7d4f7a8b6382 in service crucible [underlay IP fd00:1122:3344:102::26] (unchanged) - 93f2f40c-5616-4d8d-8519-ec6debdcede0 in service crucible [underlay IP fd00:1122:3344:102::2c] (unchanged) - ab7ba6df-d401-40bd-940e-faf57c57aa2a in service crucible [underlay IP fd00:1122:3344:102::28] (unchanged) - af322036-371f-437c-8c08-7f40f3f1403b in service crucible [underlay IP fd00:1122:3344:102::23] (unchanged) - d637264f-6f40-44c2-8b7e-a179430210d2 in service crucible [underlay IP fd00:1122:3344:102::25] (unchanged) - dce226c9-7373-4bfa-8a94-79dc472857a6 in service crucible [underlay IP fd00:1122:3344:102::27] (unchanged) - edabedf3-839c-488d-ad6f-508ffa864674 in service crucible [underlay IP fd00:1122:3344:102::24] (unchanged) - sled b59ec570-2abb-4017-80ce-129d94e7a025 -- zone config generation 2 -+ zone config generation 3 - 2d73d30e-ca47-46a8-9c12-917d4ab824b6 in service internal_ntp [underlay IP fd00:1122:3344:104::21] (unchanged) -+ 1a20ee3c-f66e-4fca-ab85-2a248aa3d79d in service crucible [underlay IP fd00:1122:3344:104::2b] (added) -+ 28852beb-d0e5-4cba-9adb-e7f0cd4bb864 in service crucible [underlay IP fd00:1122:3344:104::29] (added) -+ 45556184-7092-4a3d-873f-637976bb133b in service crucible [underlay IP fd00:1122:3344:104::22] (added) -+ 8215bf7a-10d6-4f40-aeb7-27a196307c37 in service crucible [underlay IP fd00:1122:3344:104::25] (added) -+ 9d75abfe-47ab-434a-93dd-af50dc0dddde in service crucible [underlay IP fd00:1122:3344:104::23] (added) -+ a36d291c-7f68-462f-830e-bc29e5841ce2 in service crucible [underlay IP fd00:1122:3344:104::27] (added) -+ b3a4d434-aaee-4752-8c99-69d88fbcb8c5 in service crucible [underlay IP fd00:1122:3344:104::2a] (added) -+ cf5b636b-a505-4db6-bc32-baf9f53f4371 in service crucible [underlay IP fd00:1122:3344:104::28] (added) -+ f6125d45-b9cc-4721-ba60-ed4dbb177e41 in service crucible [underlay IP fd00:1122:3344:104::26] (added) -+ f86e19d2-9145-41cf-be89-6aaa34a73873 in service crucible [underlay IP fd00:1122:3344:104::24] (added) +from: blueprint 4171ad05-89dd-474b-846b-b007e4346366 +to: blueprint f432fcd5-1284-4058-8b4a-9286a3de6163 + + ------------------------------------------------------------------------------------------------------ + zone type zone ID disposition underlay IP status + ------------------------------------------------------------------------------------------------------ + + UNCHANGED SLEDS: + + sled 41f45d9f-766e-4ca6-a881-61ee45c80f57: zones at generation 2 + crucible 322ee9f1-8903-4542-a0a8-a54cefabdeca in service fd00:1122:3344:103::24 + crucible 4ab1650f-32c5-447f-939d-64b8103a7645 in service fd00:1122:3344:103::2a + crucible 64aa65f8-1ccb-4cd6-9953-027aebdac8ff in service fd00:1122:3344:103::27 + crucible 6e811d86-8aa7-4660-935b-84b4b7721b10 in service fd00:1122:3344:103::2b + crucible 747d2426-68bf-4c22-8806-41d290b5d5f5 in service fd00:1122:3344:103::25 + crucible 7fbd2c38-5dc3-48c4-b061-558a2041d70f in service fd00:1122:3344:103::2c + crucible 8e9e923e-62b1-4cbc-9f59-d6397e338b6b in service fd00:1122:3344:103::29 + crucible b14d5478-1a0e-4b90-b526-36b06339dfc4 in service fd00:1122:3344:103::28 + crucible b40f7c7b-526c-46c8-ae33-67280c280eb7 in service fd00:1122:3344:103::23 + crucible be97b92b-38d6-422a-8c76-d37060f75bd2 in service fd00:1122:3344:103::26 + internal_ntp 267ed614-92af-4b9d-bdba-c2881c2e43a2 in service fd00:1122:3344:103::21 + nexus cc816cfe-3869-4dde-b596-397d41198628 in service fd00:1122:3344:103::22 + + sled 43677374-8d2f-4deb-8a41-eeea506db8e0: zones at generation 2 + crucible 02acbe6a-1c88-47e3-94c3-94084cbde098 in service fd00:1122:3344:101::27 + crucible 07c3c805-8888-4fe5-9543-3d2479dbe6f3 in service fd00:1122:3344:101::26 + crucible 10d98a73-ec88-4aff-a7e8-7db6a87880e6 in service fd00:1122:3344:101::24 + crucible 2a455c35-eb3c-4c73-ab6c-d0a706e25316 in service fd00:1122:3344:101::29 + crucible 3eda924f-22a9-4f3e-9a1b-91d1c47601ab in service fd00:1122:3344:101::23 + crucible 587be699-a320-4c79-b320-128d9ecddc0b in service fd00:1122:3344:101::2b + crucible 6fa06115-4959-4913-8e7b-dd70d7651f07 in service fd00:1122:3344:101::2c + crucible 8f3a1cc5-9195-4a30-ad02-b804278fe639 in service fd00:1122:3344:101::28 + crucible a1696cd4-588c-484a-b95b-66e824c0ce05 in service fd00:1122:3344:101::25 + crucible a2079cbc-a69e-41a1-b1e0-fbcb972d03f6 in service fd00:1122:3344:101::2a + internal_ntp 08c7f8aa-1ea9-469b-8cac-2fdbfc11ebcb in service fd00:1122:3344:101::21 + nexus c66ab6d5-ff7a-46d1-9fd0-70cefa352d25 in service fd00:1122:3344:101::22 + + sled 590e3034-d946-4166-b0e5-2d0034197a07: zones at generation 2 + crucible 18f8fe40-646e-4962-b17a-20e201f3a6e5 in service fd00:1122:3344:102::2a + crucible 56d5d7cf-db2c-40a3-a775-003241ad4820 in service fd00:1122:3344:102::29 + crucible 6af7f4d6-33b6-4eb3-a146-d8e9e4ae9d66 in service fd00:1122:3344:102::2b + crucible 7a9f60d3-2b66-4547-9b63-7d4f7a8b6382 in service fd00:1122:3344:102::26 + crucible 93f2f40c-5616-4d8d-8519-ec6debdcede0 in service fd00:1122:3344:102::2c + crucible ab7ba6df-d401-40bd-940e-faf57c57aa2a in service fd00:1122:3344:102::28 + crucible af322036-371f-437c-8c08-7f40f3f1403b in service fd00:1122:3344:102::23 + crucible d637264f-6f40-44c2-8b7e-a179430210d2 in service fd00:1122:3344:102::25 + crucible dce226c9-7373-4bfa-8a94-79dc472857a6 in service fd00:1122:3344:102::27 + crucible edabedf3-839c-488d-ad6f-508ffa864674 in service fd00:1122:3344:102::24 + internal_ntp 47199d48-534c-4267-a654-d2d90e64b498 in service fd00:1122:3344:102::21 + nexus 704e1fed-f8d6-4cfa-a470-bad27fdc06d1 in service fd00:1122:3344:102::22 + + MODIFIED SLEDS: + +* sled b59ec570-2abb-4017-80ce-129d94e7a025: zones at generation: 2 -> 3 + internal_ntp 2d73d30e-ca47-46a8-9c12-917d4ab824b6 in service fd00:1122:3344:104::21 ++ crucible 1a20ee3c-f66e-4fca-ab85-2a248aa3d79d in service fd00:1122:3344:104::2b added ++ crucible 28852beb-d0e5-4cba-9adb-e7f0cd4bb864 in service fd00:1122:3344:104::29 added ++ crucible 45556184-7092-4a3d-873f-637976bb133b in service fd00:1122:3344:104::22 added ++ crucible 8215bf7a-10d6-4f40-aeb7-27a196307c37 in service fd00:1122:3344:104::25 added ++ crucible 9d75abfe-47ab-434a-93dd-af50dc0dddde in service fd00:1122:3344:104::23 added ++ crucible a36d291c-7f68-462f-830e-bc29e5841ce2 in service fd00:1122:3344:104::27 added ++ crucible b3a4d434-aaee-4752-8c99-69d88fbcb8c5 in service fd00:1122:3344:104::2a added ++ crucible cf5b636b-a505-4db6-bc32-baf9f53f4371 in service fd00:1122:3344:104::28 added ++ crucible f6125d45-b9cc-4721-ba60-ed4dbb177e41 in service fd00:1122:3344:104::26 added ++ crucible f86e19d2-9145-41cf-be89-6aaa34a73873 in service fd00:1122:3344:104::24 added + + METADATA: + internal DNS version: 1 (unchanged) + external DNS version: 1 (unchanged) diff --git a/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_1_2.txt b/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_1_2.txt index 17d3db6228..380beaecf5 100644 --- a/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_1_2.txt +++ b/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_1_2.txt @@ -1,86 +1,95 @@ -diff blueprint 55502b1b-e255-438b-a16a-2680a4b5f962 blueprint 9f71f5d3-a272-4382-9154-6ea2e171a6c6 ---- blueprint 55502b1b-e255-438b-a16a-2680a4b5f962 -+++ blueprint 9f71f5d3-a272-4382-9154-6ea2e171a6c6 - sled 2d1cb4f2-cf44-40fc-b118-85036eb732a9 - zone config generation 2 - 19fbc4f8-a683-4f22-8f5a-e74782b935be in service crucible [underlay IP fd00:1122:3344:105::26] (unchanged) - 4f1ce8a2-d3a5-4a38-be4c-9817de52db37 in service crucible [underlay IP fd00:1122:3344:105::2c] (unchanged) - 6b53ab2e-d98c-485f-87a3-4d5df595390f in service crucible [underlay IP fd00:1122:3344:105::27] (unchanged) - 6dff7633-66bb-4924-a6ff-2c896e66964b in service nexus [underlay IP fd00:1122:3344:105::22] (unchanged) - 7f4e9f9f-08f8-4d14-885d-e977c05525ad in service internal_ntp [underlay IP fd00:1122:3344:105::21] (unchanged) - 93b137a1-a1d6-4b5b-b2cb-21a9f11e2883 in service crucible [underlay IP fd00:1122:3344:105::23] (unchanged) - 9f0abbad-dbd3-4d43-9675-78092217ffd9 in service crucible [underlay IP fd00:1122:3344:105::25] (unchanged) - b0c63f48-01ea-4aae-bb26-fb0dd59d1662 in service crucible [underlay IP fd00:1122:3344:105::28] (unchanged) - c406da50-34b9-4bb4-a460-8f49875d2a6a in service crucible [underlay IP fd00:1122:3344:105::24] (unchanged) - d660d7ed-28c0-45ae-9ace-dc3ecf7e8786 in service crucible [underlay IP fd00:1122:3344:105::2a] (unchanged) - e98cc0de-abf6-4da4-a20d-d05c7a9bb1d7 in service crucible [underlay IP fd00:1122:3344:105::2b] (unchanged) - f55e6aaf-e8fc-4913-9e3c-8cd1bd4bdad3 in service crucible [underlay IP fd00:1122:3344:105::29] (unchanged) - sled 48d95fef-bc9f-4f50-9a53-1e075836291d - zone config generation 2 - 094f27af-1acb-4d1e-ba97-1fc1377d4bf2 in service crucible [underlay IP fd00:1122:3344:103::2c] (unchanged) - 0dcfdfc5-481e-4153-b97c-11cf02b648ea in service crucible [underlay IP fd00:1122:3344:103::25] (unchanged) - 2aa0ea4f-3561-4989-a98c-9ab7d9a240fb in service nexus [underlay IP fd00:1122:3344:103::22] (unchanged) - 2f5e8010-a94d-43a4-9c5c-3f52832f5f7f in service crucible [underlay IP fd00:1122:3344:103::27] (unchanged) - 4a9a0a9d-87f0-4f1d-9181-27f6b435e637 in service crucible [underlay IP fd00:1122:3344:103::28] (unchanged) - 56ac1706-9e2a-49ba-bd6f-a99c44cb2ccb in service crucible [underlay IP fd00:1122:3344:103::24] (unchanged) - 67622d61-2df4-414d-aa0e-d1277265f405 in service crucible [underlay IP fd00:1122:3344:103::23] (unchanged) - 67d913e0-0005-4599-9b28-0abbf6cc2916 in service internal_ntp [underlay IP fd00:1122:3344:103::21] (unchanged) - b91b271d-8d80-4f49-99a0-34006ae86063 in service crucible [underlay IP fd00:1122:3344:103::2a] (unchanged) - d6ee1338-3127-43ec-9aaa-b973ccf05496 in service crucible [underlay IP fd00:1122:3344:103::26] (unchanged) - e39d7c9e-182b-48af-af87-58079d723583 in service crucible [underlay IP fd00:1122:3344:103::29] (unchanged) - f69f92a1-5007-4bb0-a85b-604dc217154b in service crucible [underlay IP fd00:1122:3344:103::2b] (unchanged) - sled 68d24ac5-f341-49ea-a92a-0381b52ab387 - zone config generation 2 - 01d58626-e1b0-480f-96be-ac784863c7dc in service nexus [underlay IP fd00:1122:3344:102::22] (unchanged) - 3b3c14b6-a8e2-4054-a577-8d96cb576230 in service crucible [underlay IP fd00:1122:3344:102::2c] (unchanged) - 47a87c6e-ef45-4d52-9a3e-69cdd96737cc in service crucible [underlay IP fd00:1122:3344:102::23] (unchanged) - 6464d025-4652-4948-919e-740bec5699b1 in service crucible [underlay IP fd00:1122:3344:102::24] (unchanged) - 6939ce48-b17c-4616-b176-8a419a7697be in service crucible [underlay IP fd00:1122:3344:102::29] (unchanged) - 878dfddd-3113-4197-a3ea-e0d4dbe9b476 in service crucible [underlay IP fd00:1122:3344:102::25] (unchanged) - 8d4d2b28-82bb-4e36-80da-1408d8c35d82 in service crucible [underlay IP fd00:1122:3344:102::2b] (unchanged) - 9fd52961-426f-4e62-a644-b70871103fca in service crucible [underlay IP fd00:1122:3344:102::26] (unchanged) - b44cdbc0-0ce0-46eb-8b21-a09e113aa1d0 in service crucible [underlay IP fd00:1122:3344:102::27] (unchanged) - b6b759d0-f60d-42b7-bbbc-9d61c9e895a9 in service crucible [underlay IP fd00:1122:3344:102::28] (unchanged) - c407795c-6c8b-428e-8ab8-b962913c447f in service crucible [underlay IP fd00:1122:3344:102::2a] (unchanged) - f3f2e4f3-0985-4ef6-8336-ce479382d05d in service internal_ntp [underlay IP fd00:1122:3344:102::21] (unchanged) - sled 75bc286f-2b4b-482c-9431-59272af529da -- zone config generation 2 -+ zone config generation 3 - 15bb9def-69b8-4d2e-b04f-9fee1143387c in service crucible [underlay IP fd00:1122:3344:104::25] (unchanged) - 23a8fa2b-ef3e-4017-a43f-f7a83953bd7c in service crucible [underlay IP fd00:1122:3344:104::2c] (unchanged) - 57b96d5c-b71e-43e4-8869-7d514003d00d in service internal_ntp [underlay IP fd00:1122:3344:104::21] (unchanged) - 621509d6-3772-4009-aca1-35eefd1098fb in service crucible [underlay IP fd00:1122:3344:104::28] (unchanged) - 85b8c68a-160d-461d-94dd-1baf175fa75c in service crucible [underlay IP fd00:1122:3344:104::2a] (unchanged) - 996d7570-b0df-46d5-aaa4-0c97697cf484 in service crucible [underlay IP fd00:1122:3344:104::26] (unchanged) - a732c489-d29a-4f75-b900-5966385943af in service crucible [underlay IP fd00:1122:3344:104::29] (unchanged) - b1783e95-9598-451d-b6ba-c50b52b428c3 in service crucible [underlay IP fd00:1122:3344:104::24] (unchanged) - b4947d31-f70e-4ee0-8817-0ca6cea9b16b in service nexus [underlay IP fd00:1122:3344:104::22] (unchanged) - c6dd531e-2d1d-423b-acc8-358533dab78c in service crucible [underlay IP fd00:1122:3344:104::27] (unchanged) - e4b3e159-3dbe-48cb-8497-e3da92a90e5a in service crucible [underlay IP fd00:1122:3344:104::23] (unchanged) - f0ff59e8-4105-4980-a4bb-a1f4c58de1e3 in service crucible [underlay IP fd00:1122:3344:104::2b] (unchanged) -+ 2ec75441-3d7d-4b4b-9614-af03de5a3666 in service nexus [underlay IP fd00:1122:3344:104::2d] (added) -+ 3ca5292f-8a59-4475-bb72-0f43714d0fff in service nexus [underlay IP fd00:1122:3344:104::31] (added) -+ 508abd03-cbfe-4654-9a6d-7f15a1ad32e5 in service nexus [underlay IP fd00:1122:3344:104::2e] (added) -+ 59950bc8-1497-44dd-8cbf-b6502ba921b2 in service nexus [underlay IP fd00:1122:3344:104::2f] (added) -+ 99f6d544-8599-4e2b-a55a-82d9e0034662 in service nexus [underlay IP fd00:1122:3344:104::30] (added) -+ c26b3bda-5561-44a1-a69f-22103fe209a1 in service nexus [underlay IP fd00:1122:3344:104::32] (added) - sled affab35f-600a-4109-8ea0-34a067a4e0bc -- zone config generation 2 -+ zone config generation 3 - 0dfbf374-9ef9-430f-b06d-f271bf7f84c4 in service crucible [underlay IP fd00:1122:3344:101::27] (unchanged) - 15c103f0-ac63-423b-ba5d-1b5fcd563ba3 in service nexus [underlay IP fd00:1122:3344:101::22] (unchanged) - 3aa07966-5899-4789-ace5-f8eeb375c6c3 in service crucible [underlay IP fd00:1122:3344:101::24] (unchanged) - 4ad0e9da-08f8-4d40-b4d3-d17e711b5bbf in service crucible [underlay IP fd00:1122:3344:101::29] (unchanged) - 72c5a909-077d-4ec1-a9d5-ae64ef9d716e in service crucible [underlay IP fd00:1122:3344:101::26] (unchanged) - 95482c25-1e7f-43e8-adf1-e3548a1b3ae0 in service crucible [underlay IP fd00:1122:3344:101::23] (unchanged) - a1c03689-fc62-4ea5-bb72-4d01f5138614 in service crucible [underlay IP fd00:1122:3344:101::2a] (unchanged) - a568e92e-4fbd-4b69-acd8-f16277073031 in service crucible [underlay IP fd00:1122:3344:101::2c] (unchanged) - bf79a56a-97af-4cc4-94a5-8b20d64c2cda in service crucible [underlay IP fd00:1122:3344:101::28] (unchanged) - c60379ba-4e30-4628-a79a-0ae509aef4c5 in service crucible [underlay IP fd00:1122:3344:101::25] (unchanged) - d47f4996-fac0-4657-bcea-01b1fee6404d in service crucible [underlay IP fd00:1122:3344:101::2b] (unchanged) - f1a7b9a7-fc6a-4b23-b829-045ff33117ff in service internal_ntp [underlay IP fd00:1122:3344:101::21] (unchanged) -+ 6f86d5cb-17d7-424b-9d4c-39f670532cbe in service nexus [underlay IP fd00:1122:3344:101::2e] (added) -+ 87c299eb-470e-4b6d-b8c7-6759694e66b6 in service nexus [underlay IP fd00:1122:3344:101::30] (added) -+ c72b7930-0580-4f00-93b9-8cba2c8d344e in service nexus [underlay IP fd00:1122:3344:101::2d] (added) -+ d0095508-bdb8-4faf-b091-964276a20b15 in service nexus [underlay IP fd00:1122:3344:101::31] (added) -+ ff422442-4b31-4ade-a11a-9e5a25f0404c in service nexus [underlay IP fd00:1122:3344:101::2f] (added) +from: blueprint 55502b1b-e255-438b-a16a-2680a4b5f962 +to: blueprint 9f71f5d3-a272-4382-9154-6ea2e171a6c6 + + ------------------------------------------------------------------------------------------------------ + zone type zone ID disposition underlay IP status + ------------------------------------------------------------------------------------------------------ + + UNCHANGED SLEDS: + + sled 2d1cb4f2-cf44-40fc-b118-85036eb732a9: zones at generation 2 + crucible 19fbc4f8-a683-4f22-8f5a-e74782b935be in service fd00:1122:3344:105::26 + crucible 4f1ce8a2-d3a5-4a38-be4c-9817de52db37 in service fd00:1122:3344:105::2c + crucible 6b53ab2e-d98c-485f-87a3-4d5df595390f in service fd00:1122:3344:105::27 + crucible 93b137a1-a1d6-4b5b-b2cb-21a9f11e2883 in service fd00:1122:3344:105::23 + crucible 9f0abbad-dbd3-4d43-9675-78092217ffd9 in service fd00:1122:3344:105::25 + crucible b0c63f48-01ea-4aae-bb26-fb0dd59d1662 in service fd00:1122:3344:105::28 + crucible c406da50-34b9-4bb4-a460-8f49875d2a6a in service fd00:1122:3344:105::24 + crucible d660d7ed-28c0-45ae-9ace-dc3ecf7e8786 in service fd00:1122:3344:105::2a + crucible e98cc0de-abf6-4da4-a20d-d05c7a9bb1d7 in service fd00:1122:3344:105::2b + crucible f55e6aaf-e8fc-4913-9e3c-8cd1bd4bdad3 in service fd00:1122:3344:105::29 + internal_ntp 7f4e9f9f-08f8-4d14-885d-e977c05525ad in service fd00:1122:3344:105::21 + nexus 6dff7633-66bb-4924-a6ff-2c896e66964b in service fd00:1122:3344:105::22 + + sled 48d95fef-bc9f-4f50-9a53-1e075836291d: zones at generation 2 + crucible 094f27af-1acb-4d1e-ba97-1fc1377d4bf2 in service fd00:1122:3344:103::2c + crucible 0dcfdfc5-481e-4153-b97c-11cf02b648ea in service fd00:1122:3344:103::25 + crucible 2f5e8010-a94d-43a4-9c5c-3f52832f5f7f in service fd00:1122:3344:103::27 + crucible 4a9a0a9d-87f0-4f1d-9181-27f6b435e637 in service fd00:1122:3344:103::28 + crucible 56ac1706-9e2a-49ba-bd6f-a99c44cb2ccb in service fd00:1122:3344:103::24 + crucible 67622d61-2df4-414d-aa0e-d1277265f405 in service fd00:1122:3344:103::23 + crucible b91b271d-8d80-4f49-99a0-34006ae86063 in service fd00:1122:3344:103::2a + crucible d6ee1338-3127-43ec-9aaa-b973ccf05496 in service fd00:1122:3344:103::26 + crucible e39d7c9e-182b-48af-af87-58079d723583 in service fd00:1122:3344:103::29 + crucible f69f92a1-5007-4bb0-a85b-604dc217154b in service fd00:1122:3344:103::2b + internal_ntp 67d913e0-0005-4599-9b28-0abbf6cc2916 in service fd00:1122:3344:103::21 + nexus 2aa0ea4f-3561-4989-a98c-9ab7d9a240fb in service fd00:1122:3344:103::22 + + sled 68d24ac5-f341-49ea-a92a-0381b52ab387: zones at generation 2 + crucible 3b3c14b6-a8e2-4054-a577-8d96cb576230 in service fd00:1122:3344:102::2c + crucible 47a87c6e-ef45-4d52-9a3e-69cdd96737cc in service fd00:1122:3344:102::23 + crucible 6464d025-4652-4948-919e-740bec5699b1 in service fd00:1122:3344:102::24 + crucible 6939ce48-b17c-4616-b176-8a419a7697be in service fd00:1122:3344:102::29 + crucible 878dfddd-3113-4197-a3ea-e0d4dbe9b476 in service fd00:1122:3344:102::25 + crucible 8d4d2b28-82bb-4e36-80da-1408d8c35d82 in service fd00:1122:3344:102::2b + crucible 9fd52961-426f-4e62-a644-b70871103fca in service fd00:1122:3344:102::26 + crucible b44cdbc0-0ce0-46eb-8b21-a09e113aa1d0 in service fd00:1122:3344:102::27 + crucible b6b759d0-f60d-42b7-bbbc-9d61c9e895a9 in service fd00:1122:3344:102::28 + crucible c407795c-6c8b-428e-8ab8-b962913c447f in service fd00:1122:3344:102::2a + internal_ntp f3f2e4f3-0985-4ef6-8336-ce479382d05d in service fd00:1122:3344:102::21 + nexus 01d58626-e1b0-480f-96be-ac784863c7dc in service fd00:1122:3344:102::22 + + MODIFIED SLEDS: + +* sled 75bc286f-2b4b-482c-9431-59272af529da: zones at generation: 2 -> 3 + crucible 15bb9def-69b8-4d2e-b04f-9fee1143387c in service fd00:1122:3344:104::25 + crucible 23a8fa2b-ef3e-4017-a43f-f7a83953bd7c in service fd00:1122:3344:104::2c + crucible 621509d6-3772-4009-aca1-35eefd1098fb in service fd00:1122:3344:104::28 + crucible 85b8c68a-160d-461d-94dd-1baf175fa75c in service fd00:1122:3344:104::2a + crucible 996d7570-b0df-46d5-aaa4-0c97697cf484 in service fd00:1122:3344:104::26 + crucible a732c489-d29a-4f75-b900-5966385943af in service fd00:1122:3344:104::29 + crucible b1783e95-9598-451d-b6ba-c50b52b428c3 in service fd00:1122:3344:104::24 + crucible c6dd531e-2d1d-423b-acc8-358533dab78c in service fd00:1122:3344:104::27 + crucible e4b3e159-3dbe-48cb-8497-e3da92a90e5a in service fd00:1122:3344:104::23 + crucible f0ff59e8-4105-4980-a4bb-a1f4c58de1e3 in service fd00:1122:3344:104::2b + internal_ntp 57b96d5c-b71e-43e4-8869-7d514003d00d in service fd00:1122:3344:104::21 + nexus b4947d31-f70e-4ee0-8817-0ca6cea9b16b in service fd00:1122:3344:104::22 ++ nexus 2ec75441-3d7d-4b4b-9614-af03de5a3666 in service fd00:1122:3344:104::2d added ++ nexus 3ca5292f-8a59-4475-bb72-0f43714d0fff in service fd00:1122:3344:104::31 added ++ nexus 508abd03-cbfe-4654-9a6d-7f15a1ad32e5 in service fd00:1122:3344:104::2e added ++ nexus 59950bc8-1497-44dd-8cbf-b6502ba921b2 in service fd00:1122:3344:104::2f added ++ nexus 99f6d544-8599-4e2b-a55a-82d9e0034662 in service fd00:1122:3344:104::30 added ++ nexus c26b3bda-5561-44a1-a69f-22103fe209a1 in service fd00:1122:3344:104::32 added + +* sled affab35f-600a-4109-8ea0-34a067a4e0bc: zones at generation: 2 -> 3 + crucible 0dfbf374-9ef9-430f-b06d-f271bf7f84c4 in service fd00:1122:3344:101::27 + crucible 3aa07966-5899-4789-ace5-f8eeb375c6c3 in service fd00:1122:3344:101::24 + crucible 4ad0e9da-08f8-4d40-b4d3-d17e711b5bbf in service fd00:1122:3344:101::29 + crucible 72c5a909-077d-4ec1-a9d5-ae64ef9d716e in service fd00:1122:3344:101::26 + crucible 95482c25-1e7f-43e8-adf1-e3548a1b3ae0 in service fd00:1122:3344:101::23 + crucible a1c03689-fc62-4ea5-bb72-4d01f5138614 in service fd00:1122:3344:101::2a + crucible a568e92e-4fbd-4b69-acd8-f16277073031 in service fd00:1122:3344:101::2c + crucible bf79a56a-97af-4cc4-94a5-8b20d64c2cda in service fd00:1122:3344:101::28 + crucible c60379ba-4e30-4628-a79a-0ae509aef4c5 in service fd00:1122:3344:101::25 + crucible d47f4996-fac0-4657-bcea-01b1fee6404d in service fd00:1122:3344:101::2b + internal_ntp f1a7b9a7-fc6a-4b23-b829-045ff33117ff in service fd00:1122:3344:101::21 + nexus 15c103f0-ac63-423b-ba5d-1b5fcd563ba3 in service fd00:1122:3344:101::22 ++ nexus 6f86d5cb-17d7-424b-9d4c-39f670532cbe in service fd00:1122:3344:101::2e added ++ nexus 87c299eb-470e-4b6d-b8c7-6759694e66b6 in service fd00:1122:3344:101::30 added ++ nexus c72b7930-0580-4f00-93b9-8cba2c8d344e in service fd00:1122:3344:101::2d added ++ nexus d0095508-bdb8-4faf-b091-964276a20b15 in service fd00:1122:3344:101::31 added ++ nexus ff422442-4b31-4ade-a11a-9e5a25f0404c in service fd00:1122:3344:101::2f added + + METADATA: + internal DNS version: 1 (unchanged) + external DNS version: 1 (unchanged) diff --git a/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_2_2a.txt b/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_2_2a.txt new file mode 100644 index 0000000000..58fbbd26be --- /dev/null +++ b/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_2_2a.txt @@ -0,0 +1,104 @@ +from: blueprint 9f71f5d3-a272-4382-9154-6ea2e171a6c6 +to: blueprint 9f71f5d3-a272-4382-9154-6ea2e171a6c6 + + -------------------------------------------------------------------------------------------------------- + zone type zone ID disposition underlay IP status + -------------------------------------------------------------------------------------------------------- + + UNCHANGED SLEDS: + + sled 75bc286f-2b4b-482c-9431-59272af529da: zones at generation 3 + crucible 15bb9def-69b8-4d2e-b04f-9fee1143387c in service fd00:1122:3344:104::25 + crucible 23a8fa2b-ef3e-4017-a43f-f7a83953bd7c in service fd00:1122:3344:104::2c + crucible 621509d6-3772-4009-aca1-35eefd1098fb in service fd00:1122:3344:104::28 + crucible 85b8c68a-160d-461d-94dd-1baf175fa75c in service fd00:1122:3344:104::2a + crucible 996d7570-b0df-46d5-aaa4-0c97697cf484 in service fd00:1122:3344:104::26 + crucible a732c489-d29a-4f75-b900-5966385943af in service fd00:1122:3344:104::29 + crucible b1783e95-9598-451d-b6ba-c50b52b428c3 in service fd00:1122:3344:104::24 + crucible c6dd531e-2d1d-423b-acc8-358533dab78c in service fd00:1122:3344:104::27 + crucible e4b3e159-3dbe-48cb-8497-e3da92a90e5a in service fd00:1122:3344:104::23 + crucible f0ff59e8-4105-4980-a4bb-a1f4c58de1e3 in service fd00:1122:3344:104::2b + internal_ntp 57b96d5c-b71e-43e4-8869-7d514003d00d in service fd00:1122:3344:104::21 + nexus 2ec75441-3d7d-4b4b-9614-af03de5a3666 in service fd00:1122:3344:104::2d + nexus 3ca5292f-8a59-4475-bb72-0f43714d0fff in service fd00:1122:3344:104::31 + nexus 508abd03-cbfe-4654-9a6d-7f15a1ad32e5 in service fd00:1122:3344:104::2e + nexus 59950bc8-1497-44dd-8cbf-b6502ba921b2 in service fd00:1122:3344:104::2f + nexus 99f6d544-8599-4e2b-a55a-82d9e0034662 in service fd00:1122:3344:104::30 + nexus b4947d31-f70e-4ee0-8817-0ca6cea9b16b in service fd00:1122:3344:104::22 + nexus c26b3bda-5561-44a1-a69f-22103fe209a1 in service fd00:1122:3344:104::32 + + sled affab35f-600a-4109-8ea0-34a067a4e0bc: zones at generation 3 + crucible 0dfbf374-9ef9-430f-b06d-f271bf7f84c4 in service fd00:1122:3344:101::27 + crucible 3aa07966-5899-4789-ace5-f8eeb375c6c3 in service fd00:1122:3344:101::24 + crucible 4ad0e9da-08f8-4d40-b4d3-d17e711b5bbf in service fd00:1122:3344:101::29 + crucible 72c5a909-077d-4ec1-a9d5-ae64ef9d716e in service fd00:1122:3344:101::26 + crucible 95482c25-1e7f-43e8-adf1-e3548a1b3ae0 in service fd00:1122:3344:101::23 + crucible a1c03689-fc62-4ea5-bb72-4d01f5138614 in service fd00:1122:3344:101::2a + crucible a568e92e-4fbd-4b69-acd8-f16277073031 in service fd00:1122:3344:101::2c + crucible bf79a56a-97af-4cc4-94a5-8b20d64c2cda in service fd00:1122:3344:101::28 + crucible c60379ba-4e30-4628-a79a-0ae509aef4c5 in service fd00:1122:3344:101::25 + crucible d47f4996-fac0-4657-bcea-01b1fee6404d in service fd00:1122:3344:101::2b + internal_ntp f1a7b9a7-fc6a-4b23-b829-045ff33117ff in service fd00:1122:3344:101::21 + nexus 15c103f0-ac63-423b-ba5d-1b5fcd563ba3 in service fd00:1122:3344:101::22 + nexus 6f86d5cb-17d7-424b-9d4c-39f670532cbe in service fd00:1122:3344:101::2e + nexus 87c299eb-470e-4b6d-b8c7-6759694e66b6 in service fd00:1122:3344:101::30 + nexus c72b7930-0580-4f00-93b9-8cba2c8d344e in service fd00:1122:3344:101::2d + nexus d0095508-bdb8-4faf-b091-964276a20b15 in service fd00:1122:3344:101::31 + nexus ff422442-4b31-4ade-a11a-9e5a25f0404c in service fd00:1122:3344:101::2f + + REMOVED SLEDS: + +- sled 68d24ac5-f341-49ea-a92a-0381b52ab387: zones at generation 2 +- crucible 3b3c14b6-a8e2-4054-a577-8d96cb576230 in service fd00:1122:3344:102::2c removed +- crucible 47a87c6e-ef45-4d52-9a3e-69cdd96737cc in service fd00:1122:3344:102::23 removed +- crucible 6464d025-4652-4948-919e-740bec5699b1 in service fd00:1122:3344:102::24 removed +- crucible 6939ce48-b17c-4616-b176-8a419a7697be in service fd00:1122:3344:102::29 removed +- crucible 878dfddd-3113-4197-a3ea-e0d4dbe9b476 in service fd00:1122:3344:102::25 removed +- crucible 8d4d2b28-82bb-4e36-80da-1408d8c35d82 in service fd00:1122:3344:102::2b removed +- crucible 9fd52961-426f-4e62-a644-b70871103fca in service fd00:1122:3344:102::26 removed +- crucible b44cdbc0-0ce0-46eb-8b21-a09e113aa1d0 in service fd00:1122:3344:102::27 removed +- crucible b6b759d0-f60d-42b7-bbbc-9d61c9e895a9 in service fd00:1122:3344:102::28 removed +- crucible c407795c-6c8b-428e-8ab8-b962913c447f in service fd00:1122:3344:102::2a removed +- internal_ntp f3f2e4f3-0985-4ef6-8336-ce479382d05d in service fd00:1122:3344:102::21 removed +- nexus 01d58626-e1b0-480f-96be-ac784863c7dc in service fd00:1122:3344:102::22 removed + + MODIFIED SLEDS: + +* sled 2d1cb4f2-cf44-40fc-b118-85036eb732a9: zones at generation: 2 +! warning: generation should have changed + crucible 6b53ab2e-d98c-485f-87a3-4d5df595390f in service fd00:1122:3344:105::27 + crucible 93b137a1-a1d6-4b5b-b2cb-21a9f11e2883 in service fd00:1122:3344:105::23 + crucible 9f0abbad-dbd3-4d43-9675-78092217ffd9 in service fd00:1122:3344:105::25 + crucible b0c63f48-01ea-4aae-bb26-fb0dd59d1662 in service fd00:1122:3344:105::28 + crucible c406da50-34b9-4bb4-a460-8f49875d2a6a in service fd00:1122:3344:105::24 + crucible d660d7ed-28c0-45ae-9ace-dc3ecf7e8786 in service fd00:1122:3344:105::2a + crucible e98cc0de-abf6-4da4-a20d-d05c7a9bb1d7 in service fd00:1122:3344:105::2b + crucible f55e6aaf-e8fc-4913-9e3c-8cd1bd4bdad3 in service fd00:1122:3344:105::29 +- crucible 4f1ce8a2-d3a5-4a38-be4c-9817de52db37 in service fd00:1122:3344:105::2c removed +- crucible 19fbc4f8-a683-4f22-8f5a-e74782b935be in service fd00:1122:3344:105::26 modified ++ ├─ quiesced fd00:1122:3344:105::26 +* └─ changed: disposition +- internal_ntp 7f4e9f9f-08f8-4d14-885d-e977c05525ad in service fd00:1122:3344:105::21 modified ++ ├─ in service fd01:1122:3344:105::21 +* └─ changed: underlay IP +- nexus 6dff7633-66bb-4924-a6ff-2c896e66964b in service fd00:1122:3344:105::22 modified ++ ├─ in service fd00:1122:3344:105::22 +* └─ changed: zone type config + +* sled 48d95fef-bc9f-4f50-9a53-1e075836291d: zones at generation: 2 -> 3 +- crucible 094f27af-1acb-4d1e-ba97-1fc1377d4bf2 in service fd00:1122:3344:103::2c removed +- crucible 0dcfdfc5-481e-4153-b97c-11cf02b648ea in service fd00:1122:3344:103::25 removed +- crucible 2f5e8010-a94d-43a4-9c5c-3f52832f5f7f in service fd00:1122:3344:103::27 removed +- crucible 4a9a0a9d-87f0-4f1d-9181-27f6b435e637 in service fd00:1122:3344:103::28 removed +- crucible 56ac1706-9e2a-49ba-bd6f-a99c44cb2ccb in service fd00:1122:3344:103::24 removed +- crucible 67622d61-2df4-414d-aa0e-d1277265f405 in service fd00:1122:3344:103::23 removed +- crucible b91b271d-8d80-4f49-99a0-34006ae86063 in service fd00:1122:3344:103::2a removed +- crucible d6ee1338-3127-43ec-9aaa-b973ccf05496 in service fd00:1122:3344:103::26 removed +- crucible e39d7c9e-182b-48af-af87-58079d723583 in service fd00:1122:3344:103::29 removed +- crucible f69f92a1-5007-4bb0-a85b-604dc217154b in service fd00:1122:3344:103::2b removed +- internal_ntp 67d913e0-0005-4599-9b28-0abbf6cc2916 in service fd00:1122:3344:103::21 removed +- nexus 2aa0ea4f-3561-4989-a98c-9ab7d9a240fb in service fd00:1122:3344:103::22 removed + + METADATA: + internal DNS version: 1 (unchanged) +* external DNS version: 1 -> 2 diff --git a/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_bp2.txt b/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_bp2.txt new file mode 100644 index 0000000000..46920c47f3 --- /dev/null +++ b/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_bp2.txt @@ -0,0 +1,94 @@ +blueprint 9f71f5d3-a272-4382-9154-6ea2e171a6c6 +parent: 55502b1b-e255-438b-a16a-2680a4b5f962 + + -------------------------------------------------------------------------------------------- + zone type zone ID disposition underlay IP + -------------------------------------------------------------------------------------------- + + sled 2d1cb4f2-cf44-40fc-b118-85036eb732a9: zones at generation 2 + crucible 19fbc4f8-a683-4f22-8f5a-e74782b935be in service fd00:1122:3344:105::26 + crucible 4f1ce8a2-d3a5-4a38-be4c-9817de52db37 in service fd00:1122:3344:105::2c + crucible 6b53ab2e-d98c-485f-87a3-4d5df595390f in service fd00:1122:3344:105::27 + crucible 93b137a1-a1d6-4b5b-b2cb-21a9f11e2883 in service fd00:1122:3344:105::23 + crucible 9f0abbad-dbd3-4d43-9675-78092217ffd9 in service fd00:1122:3344:105::25 + crucible b0c63f48-01ea-4aae-bb26-fb0dd59d1662 in service fd00:1122:3344:105::28 + crucible c406da50-34b9-4bb4-a460-8f49875d2a6a in service fd00:1122:3344:105::24 + crucible d660d7ed-28c0-45ae-9ace-dc3ecf7e8786 in service fd00:1122:3344:105::2a + crucible e98cc0de-abf6-4da4-a20d-d05c7a9bb1d7 in service fd00:1122:3344:105::2b + crucible f55e6aaf-e8fc-4913-9e3c-8cd1bd4bdad3 in service fd00:1122:3344:105::29 + internal_ntp 7f4e9f9f-08f8-4d14-885d-e977c05525ad in service fd00:1122:3344:105::21 + nexus 6dff7633-66bb-4924-a6ff-2c896e66964b in service fd00:1122:3344:105::22 + + sled 48d95fef-bc9f-4f50-9a53-1e075836291d: zones at generation 2 + crucible 094f27af-1acb-4d1e-ba97-1fc1377d4bf2 in service fd00:1122:3344:103::2c + crucible 0dcfdfc5-481e-4153-b97c-11cf02b648ea in service fd00:1122:3344:103::25 + crucible 2f5e8010-a94d-43a4-9c5c-3f52832f5f7f in service fd00:1122:3344:103::27 + crucible 4a9a0a9d-87f0-4f1d-9181-27f6b435e637 in service fd00:1122:3344:103::28 + crucible 56ac1706-9e2a-49ba-bd6f-a99c44cb2ccb in service fd00:1122:3344:103::24 + crucible 67622d61-2df4-414d-aa0e-d1277265f405 in service fd00:1122:3344:103::23 + crucible b91b271d-8d80-4f49-99a0-34006ae86063 in service fd00:1122:3344:103::2a + crucible d6ee1338-3127-43ec-9aaa-b973ccf05496 in service fd00:1122:3344:103::26 + crucible e39d7c9e-182b-48af-af87-58079d723583 in service fd00:1122:3344:103::29 + crucible f69f92a1-5007-4bb0-a85b-604dc217154b in service fd00:1122:3344:103::2b + internal_ntp 67d913e0-0005-4599-9b28-0abbf6cc2916 in service fd00:1122:3344:103::21 + nexus 2aa0ea4f-3561-4989-a98c-9ab7d9a240fb in service fd00:1122:3344:103::22 + + sled 68d24ac5-f341-49ea-a92a-0381b52ab387: zones at generation 2 + crucible 3b3c14b6-a8e2-4054-a577-8d96cb576230 in service fd00:1122:3344:102::2c + crucible 47a87c6e-ef45-4d52-9a3e-69cdd96737cc in service fd00:1122:3344:102::23 + crucible 6464d025-4652-4948-919e-740bec5699b1 in service fd00:1122:3344:102::24 + crucible 6939ce48-b17c-4616-b176-8a419a7697be in service fd00:1122:3344:102::29 + crucible 878dfddd-3113-4197-a3ea-e0d4dbe9b476 in service fd00:1122:3344:102::25 + crucible 8d4d2b28-82bb-4e36-80da-1408d8c35d82 in service fd00:1122:3344:102::2b + crucible 9fd52961-426f-4e62-a644-b70871103fca in service fd00:1122:3344:102::26 + crucible b44cdbc0-0ce0-46eb-8b21-a09e113aa1d0 in service fd00:1122:3344:102::27 + crucible b6b759d0-f60d-42b7-bbbc-9d61c9e895a9 in service fd00:1122:3344:102::28 + crucible c407795c-6c8b-428e-8ab8-b962913c447f in service fd00:1122:3344:102::2a + internal_ntp f3f2e4f3-0985-4ef6-8336-ce479382d05d in service fd00:1122:3344:102::21 + nexus 01d58626-e1b0-480f-96be-ac784863c7dc in service fd00:1122:3344:102::22 + + sled 75bc286f-2b4b-482c-9431-59272af529da: zones at generation 3 + crucible 15bb9def-69b8-4d2e-b04f-9fee1143387c in service fd00:1122:3344:104::25 + crucible 23a8fa2b-ef3e-4017-a43f-f7a83953bd7c in service fd00:1122:3344:104::2c + crucible 621509d6-3772-4009-aca1-35eefd1098fb in service fd00:1122:3344:104::28 + crucible 85b8c68a-160d-461d-94dd-1baf175fa75c in service fd00:1122:3344:104::2a + crucible 996d7570-b0df-46d5-aaa4-0c97697cf484 in service fd00:1122:3344:104::26 + crucible a732c489-d29a-4f75-b900-5966385943af in service fd00:1122:3344:104::29 + crucible b1783e95-9598-451d-b6ba-c50b52b428c3 in service fd00:1122:3344:104::24 + crucible c6dd531e-2d1d-423b-acc8-358533dab78c in service fd00:1122:3344:104::27 + crucible e4b3e159-3dbe-48cb-8497-e3da92a90e5a in service fd00:1122:3344:104::23 + crucible f0ff59e8-4105-4980-a4bb-a1f4c58de1e3 in service fd00:1122:3344:104::2b + internal_ntp 57b96d5c-b71e-43e4-8869-7d514003d00d in service fd00:1122:3344:104::21 + nexus 2ec75441-3d7d-4b4b-9614-af03de5a3666 in service fd00:1122:3344:104::2d + nexus 3ca5292f-8a59-4475-bb72-0f43714d0fff in service fd00:1122:3344:104::31 + nexus 508abd03-cbfe-4654-9a6d-7f15a1ad32e5 in service fd00:1122:3344:104::2e + nexus 59950bc8-1497-44dd-8cbf-b6502ba921b2 in service fd00:1122:3344:104::2f + nexus 99f6d544-8599-4e2b-a55a-82d9e0034662 in service fd00:1122:3344:104::30 + nexus b4947d31-f70e-4ee0-8817-0ca6cea9b16b in service fd00:1122:3344:104::22 + nexus c26b3bda-5561-44a1-a69f-22103fe209a1 in service fd00:1122:3344:104::32 + + sled affab35f-600a-4109-8ea0-34a067a4e0bc: zones at generation 3 + crucible 0dfbf374-9ef9-430f-b06d-f271bf7f84c4 in service fd00:1122:3344:101::27 + crucible 3aa07966-5899-4789-ace5-f8eeb375c6c3 in service fd00:1122:3344:101::24 + crucible 4ad0e9da-08f8-4d40-b4d3-d17e711b5bbf in service fd00:1122:3344:101::29 + crucible 72c5a909-077d-4ec1-a9d5-ae64ef9d716e in service fd00:1122:3344:101::26 + crucible 95482c25-1e7f-43e8-adf1-e3548a1b3ae0 in service fd00:1122:3344:101::23 + crucible a1c03689-fc62-4ea5-bb72-4d01f5138614 in service fd00:1122:3344:101::2a + crucible a568e92e-4fbd-4b69-acd8-f16277073031 in service fd00:1122:3344:101::2c + crucible bf79a56a-97af-4cc4-94a5-8b20d64c2cda in service fd00:1122:3344:101::28 + crucible c60379ba-4e30-4628-a79a-0ae509aef4c5 in service fd00:1122:3344:101::25 + crucible d47f4996-fac0-4657-bcea-01b1fee6404d in service fd00:1122:3344:101::2b + internal_ntp f1a7b9a7-fc6a-4b23-b829-045ff33117ff in service fd00:1122:3344:101::21 + nexus 15c103f0-ac63-423b-ba5d-1b5fcd563ba3 in service fd00:1122:3344:101::22 + nexus 6f86d5cb-17d7-424b-9d4c-39f670532cbe in service fd00:1122:3344:101::2e + nexus 87c299eb-470e-4b6d-b8c7-6759694e66b6 in service fd00:1122:3344:101::30 + nexus c72b7930-0580-4f00-93b9-8cba2c8d344e in service fd00:1122:3344:101::2d + nexus d0095508-bdb8-4faf-b091-964276a20b15 in service fd00:1122:3344:101::31 + nexus ff422442-4b31-4ade-a11a-9e5a25f0404c in service fd00:1122:3344:101::2f + +METADATA: + created by: test_blueprint2 + created at: 1970-01-01T00:00:00.000Z + comment: (none) + internal DNS version: 1 + external DNS version: 1 diff --git a/nexus/types/Cargo.toml b/nexus/types/Cargo.toml index ecc180b6db..aff45d07de 100644 --- a/nexus/types/Cargo.toml +++ b/nexus/types/Cargo.toml @@ -19,6 +19,7 @@ serde_json.workspace = true serde_with.workspace = true steno.workspace = true strum.workspace = true +tabled.workspace = true thiserror.workspace = true uuid.workspace = true diff --git a/nexus/types/src/deployment.rs b/nexus/types/src/deployment.rs index 22eb6b7dbc..4c4f3823c6 100644 --- a/nexus/types/src/deployment.rs +++ b/nexus/types/src/deployment.rs @@ -29,11 +29,14 @@ use omicron_common::api::external::Generation; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; +use sled_agent_client::ZoneKind; use std::collections::BTreeMap; use std::collections::BTreeSet; +use std::collections::HashMap; use std::fmt; use strum::EnumIter; use strum::IntoEnumIterator; +use thiserror::Error; use uuid::Uuid; /// Fleet-wide deployment policy @@ -170,6 +173,19 @@ pub struct Blueprint { } impl Blueprint { + /// Return metadata for this blueprint. + pub fn metadata(&self) -> BlueprintMetadata { + BlueprintMetadata { + id: self.id, + parent_blueprint_id: self.parent_blueprint_id, + internal_dns_version: self.internal_dns_version, + external_dns_version: self.external_dns_version, + time_created: self.time_created, + creator: self.creator.clone(), + comment: self.comment.clone(), + } + } + /// Iterate over the [`BlueprintZoneConfig`] instances in the blueprint /// that match the provided filter, along with the associated sled id. pub fn all_blueprint_zones( @@ -198,36 +214,42 @@ impl Blueprint { self.blueprint_zones.keys().copied() } - /// Summarize the difference between sleds and zones between two blueprints - pub fn diff_sleds<'a>( - &'a self, - other: &'a Blueprint, - ) -> OmicronZonesDiff<'a> { - OmicronZonesDiff { - before_label: format!("blueprint {}", self.id), - before_zones: self.blueprint_zones.clone(), - after_label: format!("blueprint {}", other.id), - after_zones: &other.blueprint_zones, - } + /// Summarize the difference between sleds and zones between two + /// blueprints. + /// + /// The argument provided is the "before" side, and `self` is the "after" + /// side. This matches the order of arguments to + /// [`Blueprint::diff_since_collection`]. + pub fn diff_since_blueprint( + &self, + before: &Blueprint, + ) -> Result { + BlueprintDiff::new( + DiffBeforeMetadata::Blueprint(Box::new(before.metadata())), + before.blueprint_zones.clone(), + self.metadata(), + self.blueprint_zones.clone(), + ) } /// Summarize the differences in sleds and zones between a collection and a - /// blueprint + /// blueprint. /// /// This gives an idea about what would change about a running system if /// one were to execute the blueprint. /// - /// Note that collections do not currently include information about what - /// zones are in-service, so it is assumed that all zones in the collection - /// are in-service. (This is the same assumption made by + /// Note that collections do not include information about zone + /// disposition, so it is assumed that all zones in the collection have the + /// [`InService`](BlueprintZoneDisposition::InService) disposition. (This + /// is the same assumption made by /// [`BlueprintZonesConfig::initial_from_collection`]. The logic here may /// also be expanded to handle cases where not all zones in the collection /// are in-service.) - pub fn diff_sleds_from_collection( + pub fn diff_since_collection( &self, - collection: &Collection, - ) -> OmicronZonesDiff<'_> { - let before_zones = collection + before: &Collection, + ) -> Result { + let before_zones = before .omicron_zones .iter() .map(|(sled_id, zones_found)| { @@ -247,12 +269,13 @@ impl Blueprint { (*sled_id, zones) }) .collect(); - OmicronZonesDiff { - before_label: format!("collection {}", collection.id), + + BlueprintDiff::new( + DiffBeforeMetadata::Collection { id: before.id }, before_zones, - after_label: format!("blueprint {}", self.id), - after_zones: &self.blueprint_zones, - } + self.metadata(), + self.blueprint_zones.clone(), + ) } /// Return a struct that can be displayed to present information about the @@ -283,35 +306,11 @@ impl<'a> fmt::Display for BlueprintDisplay<'a> { .map(|u| u.to_string()) .unwrap_or_else(|| String::from("")) )?; - writeln!( - f, - "created by {}{}", - b.creator, - if b.creator.parse::().is_ok() { - " (likely a Nexus instance)" - } else { - "" - } - )?; - writeln!( - f, - "created at {}", - humantime::format_rfc3339_millis(b.time_created.into(),) - )?; - writeln!(f, "internal DNS version: {}", b.internal_dns_version)?; - writeln!(f, "comment: {}", b.comment)?; - writeln!(f, "zones:\n")?; - for (sled_id, sled_zones) in &b.blueprint_zones { - writeln!( - f, - " sled {}: Omicron zones at generation {}", - sled_id, sled_zones.generation - )?; - for z in &sled_zones.zones { - writeln!(f, " {}", z.display())?; - } - } + writeln!(f, "\n{}", self.make_zone_table())?; + + writeln!(f, "\n{}", table_display::metadata_heading())?; + writeln!(f, "{}", self.make_metadata_table())?; Ok(()) } @@ -339,7 +338,8 @@ impl BlueprintZonesConfig { /// Constructs a new [`BlueprintZonesConfig`] from a collection's zones. /// /// For the initial blueprint, all zones within a collection are assumed to - /// be in-service. + /// have the [`InService`](BlueprintZoneDisposition::InService) + /// disposition. pub fn initial_from_collection(collection: &OmicronZonesConfig) -> Self { let zones = collection .zones @@ -364,10 +364,10 @@ impl BlueprintZonesConfig { /// Sorts the list of zones stored in this configuration. /// - /// This is not strictly necessary. But for testing, it's helpful for - /// zones to be in sorted order. + /// This is not strictly necessary. But for testing (particularly snapshot + /// testing), it's helpful for zones to be in sorted order. pub fn sort(&mut self) { - self.zones.sort_unstable_by_key(|z| z.config.id); + self.zones.sort_unstable_by_key(zone_sort_key); } /// Converts self to an [`OmicronZonesConfig`], applying the provided @@ -392,6 +392,12 @@ impl BlueprintZonesConfig { } } +fn zone_sort_key(z: &BlueprintZoneConfig) -> impl Ord { + // First sort by kind, then by ID. This makes it so that zones of the same + // kind (e.g. Crucible zones) are grouped together. + (z.config.zone_type.kind(), z.config.id) +} + /// Describes one Omicron-managed zone in a blueprint. /// /// This is a wrapper around an [`OmicronZoneConfig`] that also includes a @@ -407,39 +413,6 @@ pub struct BlueprintZoneConfig { pub disposition: BlueprintZoneDisposition, } -impl BlueprintZoneConfig { - /// Return a struct that can be displayed to present information about the - /// zone. - pub fn display(&self) -> BlueprintZoneConfigDisplay<'_> { - BlueprintZoneConfigDisplay { zone: self } - } -} - -/// A wrapper to allow a [`BlueprintZoneConfig`] to be displayed with -/// information. -/// -/// Returned by [`BlueprintZoneConfig::display()`]. -#[derive(Clone, Debug)] -#[must_use = "this struct does nothing unless displayed"] -pub struct BlueprintZoneConfigDisplay<'a> { - zone: &'a BlueprintZoneConfig, -} - -impl<'a> fmt::Display for BlueprintZoneConfigDisplay<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let z = self.zone; - write!( - f, - "{} {: bool { // This code could be written in three ways: @@ -573,6 +543,12 @@ pub struct BlueprintMetadata { pub comment: String, } +impl BlueprintMetadata { + pub fn display_id(&self) -> String { + format!("blueprint {}", self.id) + } +} + /// Describes what blueprint, if any, the system is currently working toward #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)] pub struct BlueprintTarget { @@ -595,75 +571,386 @@ pub struct BlueprintTargetSet { /// Summarizes the differences between two blueprints #[derive(Debug)] -pub struct OmicronZonesDiff<'a> { - before_label: String, - // We store an owned copy of "before_zones" to make it easier to support - // collections here, where we need to assemble this map ourselves. - before_zones: BTreeMap, - after_label: String, - after_zones: &'a BTreeMap, +pub struct BlueprintDiff { + before_meta: DiffBeforeMetadata, + after_meta: BlueprintMetadata, + sleds: DiffSleds, +} + +impl BlueprintDiff { + /// Build a diff with the provided contents, verifying that the provided + /// data is valid. + fn new( + before_meta: DiffBeforeMetadata, + before_zones: BTreeMap, + after_meta: BlueprintMetadata, + after_zones: BTreeMap, + ) -> Result { + let mut errors = Vec::new(); + + let sleds = DiffSleds::new(before_zones, after_zones, &mut errors); + + if errors.is_empty() { + Ok(Self { before_meta, after_meta, sleds }) + } else { + Err(BlueprintDiffError { + before_meta, + after_meta: Box::new(after_meta), + errors, + }) + } + } + + /// Returns metadata about the source of the "before" data. + pub fn before_meta(&self) -> &DiffBeforeMetadata { + &self.before_meta + } + + /// Returns metadata about the source of the "after" data. + pub fn after_meta(&self) -> &BlueprintMetadata { + &self.after_meta + } + + /// Iterate over sleds only present in the second blueprint of a diff + pub fn sleds_added( + &self, + ) -> impl ExactSizeIterator + '_ { + self.sleds.added.iter().map(|(sled_id, zones)| (*sled_id, zones)) + } + + /// Iterate over sleds only present in the first blueprint of a diff + pub fn sleds_removed( + &self, + ) -> impl ExactSizeIterator + '_ { + self.sleds.removed.iter().map(|(sled_id, zones)| (*sled_id, zones)) + } + + /// Iterate over sleds present in both blueprints in a diff that have + /// changes. + pub fn sleds_modified( + &self, + ) -> impl ExactSizeIterator + '_ { + self.sleds.modified.iter().map(|(sled_id, sled)| (*sled_id, sled)) + } + + /// Iterate over sleds present in both blueprints in a diff that have no + /// changes. + pub fn sleds_unchanged( + &self, + ) -> impl Iterator + '_ { + self.sleds.unchanged.iter().map(|(sled_id, zones)| (*sled_id, zones)) + } + + /// Return a struct that can be used to display the diff. + pub fn display(&self) -> BlueprintDiffDisplay<'_> { + BlueprintDiffDisplay::new(self) + } } -/// Describes a sled that appeared on both sides of a diff (possibly changed) #[derive(Debug)] -pub struct DiffSledCommon<'a> { +struct DiffSleds { + added: BTreeMap, + removed: BTreeMap, + modified: BTreeMap, + unchanged: BTreeMap, +} + +impl DiffSleds { + /// Builds added, removed and common maps, verifying that the provided data + /// is valid. + /// + /// The return value only contains the sleds that are present in both + /// blueprints. + fn new( + before: BTreeMap, + mut after: BTreeMap, + errors: &mut Vec, + ) -> Self { + let mut removed = BTreeMap::new(); + let mut modified = BTreeMap::new(); + let mut unchanged = BTreeMap::new(); + + for (sled_id, mut before_z) in before { + if let Some(mut after_z) = after.remove(&sled_id) { + // Sort before_z and after_z so they can be compared directly. + before_z.sort(); + after_z.sort(); + + if before_z == after_z { + unchanged.insert(sled_id, before_z); + } else { + let sled_modified = DiffSledModified::new( + sled_id, before_z, after_z, errors, + ); + modified.insert(sled_id, sled_modified); + } + } else { + removed.insert(sled_id, before_z); + } + } + + // We removed everything common from `after` above, so anything left is + // an added sled. + Self { added: after, removed, modified, unchanged } + } +} + +/// Wrapper to allow a [`BlueprintDiff`] to be displayed. +/// +/// Returned by [`BlueprintDiff::display()`]. +#[derive(Clone, Debug)] +#[must_use = "this struct does nothing unless displayed"] +pub struct BlueprintDiffDisplay<'diff> { + diff: &'diff BlueprintDiff, + // TODO: add colorization with a stylesheet +} + +impl<'diff> BlueprintDiffDisplay<'diff> { + #[inline] + fn new(diff: &'diff BlueprintDiff) -> Self { + Self { diff } + } +} + +impl<'diff> fmt::Display for BlueprintDiffDisplay<'diff> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let diff = self.diff; + + // Print things differently based on whether the diff is between a + // collection and a blueprint, or a blueprint and a blueprint. + match &diff.before_meta { + DiffBeforeMetadata::Collection { id } => { + writeln!( + f, + "from: collection {}\n\ + to: blueprint {}", + id, diff.after_meta.id, + )?; + } + DiffBeforeMetadata::Blueprint(before) => { + writeln!( + f, + "from: blueprint {}\n\ + to: blueprint {}", + before.id, diff.after_meta.id + )?; + } + } + + writeln!(f, "\n{}", self.make_zone_diff_table())?; + + writeln!(f, "\n{}", table_display::metadata_diff_heading())?; + writeln!(f, "{}", self.make_metadata_diff_table())?; + + Ok(()) + } +} + +#[derive(Clone, Debug, Error)] +pub struct BlueprintDiffError { + pub before_meta: DiffBeforeMetadata, + pub after_meta: Box, + pub errors: Vec, +} + +impl fmt::Display for BlueprintDiffError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!( + f, + "errors in diff between {} and {}:", + self.before_meta.display_id(), + self.after_meta.display_id() + )?; + for e in &self.errors { + writeln!(f, " - {}", e)?; + } + Ok(()) + } +} + +/// An individual error within a [`BlueprintDiffError`]. +#[derive(Clone, Debug)] +pub enum BlueprintDiffSingleError { + /// The [`OmicronZoneType`] of a particular zone changed between the before + /// and after blueprints. + /// + /// For a particular zone, the type should never change. + ZoneTypeChanged { + sled_id: Uuid, + zone_id: Uuid, + before: ZoneKind, + after: ZoneKind, + }, +} + +impl fmt::Display for BlueprintDiffSingleError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BlueprintDiffSingleError::ZoneTypeChanged { + sled_id, + zone_id, + before, + after, + } => write!( + f, + "on sled {}, zone {} changed type from {} to {}", + zone_id, sled_id, before, after + ), + } + } +} + +/// Data about the "before" version within a [`BlueprintDiff`]. +#[derive(Clone, Debug)] +pub enum DiffBeforeMetadata { + /// The diff was made from a collection. + Collection { id: Uuid }, + /// The diff was made from a blueprint. + Blueprint(Box), +} + +impl DiffBeforeMetadata { + pub fn display_id(&self) -> String { + match self { + DiffBeforeMetadata::Collection { id } => format!("collection {id}"), + DiffBeforeMetadata::Blueprint(b) => b.display_id(), + } + } +} + +/// Describes a sled that appeared on both sides of a diff and is changed. +#[derive(Clone, Debug)] +pub struct DiffSledModified { /// id of the sled pub sled_id: Uuid, /// generation of the "zones" configuration on the left side pub generation_before: Generation, /// generation of the "zones" configuration on the right side pub generation_after: Generation, - zones_added: Vec<&'a BlueprintZoneConfig>, - zones_removed: Vec<&'a BlueprintZoneConfig>, - zones_common: Vec>, + zones_added: Vec, + zones_removed: Vec, + zones_common: Vec, } -impl<'a> DiffSledCommon<'a> { +impl DiffSledModified { + fn new( + sled_id: Uuid, + before: BlueprintZonesConfig, + after: BlueprintZonesConfig, + errors: &mut Vec, + ) -> Self { + // Assemble separate summaries of the zones, indexed by zone id. + let before_by_id: HashMap<_, _> = before + .zones + .into_iter() + .map(|zone| (zone.config.id, zone)) + .collect(); + let mut after_by_id: HashMap<_, _> = after + .zones + .into_iter() + .map(|zone| (zone.config.id, zone)) + .collect(); + + let mut zones_removed = Vec::new(); + let mut zones_common = Vec::new(); + + // Now go through each zone and compare them. + for (zone_id, zone_before) in before_by_id { + if let Some(zone_after) = after_by_id.remove(&zone_id) { + let before_kind = zone_before.config.zone_type.kind(); + let after_kind = zone_after.config.zone_type.kind(); + + if before_kind != after_kind { + errors.push(BlueprintDiffSingleError::ZoneTypeChanged { + sled_id, + zone_id, + before: before_kind, + after: after_kind, + }); + } else { + let common = DiffZoneCommon { zone_before, zone_after }; + zones_common.push(common); + } + } else { + zones_removed.push(zone_before); + } + } + + // Since we removed common zones above, anything else exists only in + // before and was therefore added. + let mut zones_added: Vec<_> = after_by_id.into_values().collect(); + + // Sort for test reproducibility. + zones_added.sort_unstable_by_key(zone_sort_key); + zones_removed.sort_unstable_by_key(zone_sort_key); + zones_common.sort_unstable_by_key(|common| { + // The ID is common by definition, and the zone type was already + // verified to be the same above. So just sort by the sort key for + // the before zone. (In case of errors, the result will be thrown + // away anyway, so this is harmless.) + zone_sort_key(&common.zone_before) + }); + + Self { + sled_id, + generation_before: before.generation, + generation_after: after.generation, + zones_added, + zones_removed, + zones_common, + } + } + /// Iterate over zones added between the blueprints pub fn zones_added( &self, - ) -> impl Iterator + '_ { - self.zones_added.iter().copied() + ) -> impl ExactSizeIterator + '_ { + self.zones_added.iter() } /// Iterate over zones removed between the blueprints pub fn zones_removed( &self, - ) -> impl Iterator + '_ { - self.zones_removed.iter().copied() + ) -> impl ExactSizeIterator + '_ { + self.zones_removed.iter() } /// Iterate over zones that are common to both blueprints pub fn zones_in_common( &self, - ) -> impl Iterator> + '_ { - self.zones_common.iter().copied() + ) -> impl ExactSizeIterator + '_ { + self.zones_common.iter() } - /// Iterate over zones that changed between the blue prints - pub fn zones_changed( + /// Iterate over zones that changed between the blueprints + pub fn zones_modified(&self) -> impl Iterator + '_ { + self.zones_in_common().filter(|z| z.is_modified()) + } + + /// Iterate over zones that did not change between the blueprints + pub fn zones_unchanged( &self, - ) -> impl Iterator> + '_ { - self.zones_in_common().filter(|z| z.is_changed()) + ) -> impl Iterator + '_ { + self.zones_in_common().filter(|z| !z.is_modified()) } } /// Describes a zone that was common to both sides of a diff -#[derive(Debug, Copy, Clone)] -pub struct DiffZoneCommon<'a> { +#[derive(Debug, Clone)] +pub struct DiffZoneCommon { /// full zone configuration before - pub zone_before: &'a BlueprintZoneConfig, + pub zone_before: BlueprintZoneConfig, /// full zone configuration after - pub zone_after: &'a BlueprintZoneConfig, + pub zone_after: BlueprintZoneConfig, } -impl<'a> DiffZoneCommon<'a> { +impl DiffZoneCommon { /// Returns true if there are any differences between `zone_before` and /// `zone_after`. /// /// This is equivalent to `config_changed() || disposition_changed()`. #[inline] - pub fn is_changed(&self) -> bool { + pub fn is_modified(&self) -> bool { // state is smaller and easier to compare than config. self.disposition_changed() || self.config_changed() } @@ -682,253 +969,673 @@ impl<'a> DiffZoneCommon<'a> { } } -impl<'a> OmicronZonesDiff<'a> { - fn sleds_before(&self) -> BTreeSet { - self.before_zones.keys().copied().collect() - } +/// Encapsulates Reconfigurator state +/// +/// This serialized from is intended for saving state from hand-constructed or +/// real, deployed systems and loading it back into a simulator or test suite +/// +/// **This format is not stable. It may change at any time without +/// backwards-compatibility guarantees.** +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UnstableReconfiguratorState { + pub policy: Policy, + pub collections: Vec, + pub blueprints: Vec, + pub internal_dns: BTreeMap, + pub external_dns: BTreeMap, + pub silo_names: Vec, + pub external_dns_zone_names: Vec, +} - fn sleds_after(&self) -> BTreeSet { - self.after_zones.keys().copied().collect() - } +/// Code to generate tables. +/// +/// This is here because `tabled` has a number of generically-named types, and +/// we'd like to avoid name collisions with other types. +mod table_display { + use super::*; + use crate::sectioned_table::SectionSpacing; + use crate::sectioned_table::StBuilder; + use crate::sectioned_table::StSectionBuilder; + use tabled::builder::Builder; + use tabled::settings::object::Columns; + use tabled::settings::Modify; + use tabled::settings::Padding; + use tabled::settings::Style; + use tabled::Table; + + impl<'a> super::BlueprintDisplay<'a> { + pub(super) fn make_zone_table(&self) -> Table { + let blueprint_zones = &self.blueprint.blueprint_zones; + let mut builder = StBuilder::new(); + builder.push_header_row(header_row()); + + for (sled_id, sled_zones) in blueprint_zones { + let heading = format!( + "{SLED_INDENT}sled {sled_id}: zones at generation {}", + sled_zones.generation + ); + builder.make_section( + SectionSpacing::Always, + heading, + |section| { + for zone in &sled_zones.zones { + add_zone_record( + ZONE_INDENT.to_string(), + zone, + section, + ); + } + + if section.is_empty() { + section.push_nested_heading( + SectionSpacing::IfNotFirst, + format!("{ZONE_HEAD_INDENT}{NO_ZONES_PARENS}"), + ); + } + }, + ); + } - /// Iterate over sleds only present in the second blueprint of a diff - pub fn sleds_added( - &self, - ) -> impl Iterator + '_ { - let sled_ids = self - .sleds_after() - .difference(&self.sleds_before()) - .copied() - .collect::>(); + builder.build() + } - sled_ids - .into_iter() - .map(|sled_id| (sled_id, self.after_zones.get(&sled_id).unwrap())) + pub(super) fn make_metadata_table(&self) -> Table { + let mut builder = Builder::new(); + + // Metadata is presented as a linear (top-to-bottom) table with a + // small indent. + + builder.push_record(vec![ + METADATA_INDENT.to_string(), + linear_table_label(&CREATED_BY), + self.blueprint.creator.clone(), + ]); + + builder.push_record(vec![ + METADATA_INDENT.to_string(), + linear_table_label(&CREATED_AT), + humantime::format_rfc3339_millis( + self.blueprint.time_created.into(), + ) + .to_string(), + ]); + + let comment = if self.blueprint.comment.is_empty() { + NONE_PARENS.to_string() + } else { + self.blueprint.comment.clone() + }; + + builder.push_record(vec![ + METADATA_INDENT.to_string(), + linear_table_label(&COMMENT), + comment, + ]); + + builder.push_record(vec![ + METADATA_INDENT.to_string(), + linear_table_label(&INTERNAL_DNS_VERSION), + self.blueprint.internal_dns_version.to_string(), + ]); + + builder.push_record(vec![ + METADATA_INDENT.to_string(), + linear_table_label(&EXTERNAL_DNS_VERSION), + self.blueprint.external_dns_version.to_string(), + ]); + + let mut table = builder.build(); + apply_linear_table_settings(&mut table); + table + } } - /// Iterate over sleds only present in the first blueprint of a diff - pub fn sleds_removed( - &self, - ) -> impl Iterator + '_ { - let sled_ids = self - .sleds_before() - .difference(&self.sleds_after()) - .copied() - .collect::>(); - sled_ids - .into_iter() - .map(|sled_id| (sled_id, self.before_zones.get(&sled_id).unwrap())) - } - - /// Iterate over sleds present in both blueprints in a diff - pub fn sleds_in_common( - &'a self, - ) -> impl Iterator)> + '_ { - let sled_ids = self - .sleds_before() - .intersection(&self.sleds_after()) - .copied() - .collect::>(); - sled_ids.into_iter().map(|sled_id| { - let b1sledzones = self.before_zones.get(&sled_id).unwrap(); - let b2sledzones = self.after_zones.get(&sled_id).unwrap(); - - // Assemble separate summaries of the zones, indexed by zone id. - let b1_zones: BTreeMap = b1sledzones - .zones - .iter() - .map(|zone| (zone.config.id, zone)) - .collect(); - let mut b2_zones: BTreeMap = - b2sledzones - .zones - .iter() - .map(|zone| (zone.config.id, zone)) - .collect(); - let mut zones_removed = vec![]; - let mut zones_common = vec![]; - - // Now go through each zone and compare them. - for (zone_id, zone_before) in &b1_zones { - if let Some(zone_after) = b2_zones.remove(zone_id) { - zones_common - .push(DiffZoneCommon { zone_before, zone_after }); - } else { - zones_removed.push(*zone_before); + impl<'diff> BlueprintDiffDisplay<'diff> { + pub(super) fn make_zone_diff_table(&self) -> Table { + let diff = self.diff; + + // Add the unchanged prefix to the zone indent since the first + // column will be used as the prefix. + let mut builder = StBuilder::new(); + builder.push_header_row(diff_header_row()); + + // The order is: + // + // 1. Unchanged + // 2. Removed + // 3. Modified + // 4. Added + // + // The idea behind the order is to (a) group all changes together + // and (b) put changes towards the bottom, so people have to scroll + // back less. + // + // Zones within a modified sled follow the same order. If you're + // changing the order here, make sure to keep that in sync. + + // First, unchanged sleds. + builder.make_section( + SectionSpacing::Always, + unchanged_sleds_heading(), + |section| { + for (sled_id, sled_zones) in diff.sleds_unchanged() { + add_whole_sled_records( + sled_id, + sled_zones, + WholeSledKind::Unchanged, + section, + ); + } + }, + ); + + // Then, removed sleds. + builder.make_section( + SectionSpacing::Always, + removed_sleds_heading(), + |section| { + for (sled_id, sled_zones) in diff.sleds_removed() { + add_whole_sled_records( + sled_id, + sled_zones, + WholeSledKind::Removed, + section, + ); + } + }, + ); + + // Then, modified sleds. + builder.make_section( + SectionSpacing::Always, + modified_sleds_heading(), + |section| { + // For sleds that are in common: + for (sled_id, modified) in diff.sleds_modified() { + add_modified_sled_records(sled_id, modified, section); + } + }, + ); + + // Finally, added sleds. + builder.make_section( + SectionSpacing::Always, + added_sleds_heading(), + |section| { + for (sled_id, sled_zones) in diff.sleds_added() { + add_whole_sled_records( + sled_id, + sled_zones, + WholeSledKind::Added, + section, + ); + } + }, + ); + + builder.build() + } + + pub(super) fn make_metadata_diff_table(&self) -> Table { + let diff = self.diff; + let mut builder = Builder::new(); + + // Metadata is presented as a linear (top-to-bottom) table with a + // small indent. + + match &diff.before_meta { + DiffBeforeMetadata::Collection { .. } => { + // Collections don't have DNS versions, so this is new. + builder.push_record(vec![ + format!("{ADDED_PREFIX}{METADATA_DIFF_INDENT}"), + metadata_table_internal_dns(), + linear_table_modified( + &NOT_PRESENT_IN_COLLECTION_PARENS, + &diff.after_meta.internal_dns_version, + ), + ]); + + builder.push_record(vec![ + format!("{ADDED_PREFIX}{METADATA_DIFF_INDENT}"), + metadata_table_external_dns(), + linear_table_modified( + &NOT_PRESENT_IN_COLLECTION_PARENS, + &diff.after_meta.external_dns_version, + ), + ]); + } + DiffBeforeMetadata::Blueprint(before) => { + if before.internal_dns_version + != diff.after_meta.internal_dns_version + { + builder.push_record(vec![ + format!("{MODIFIED_PREFIX}{METADATA_DIFF_INDENT}"), + metadata_table_internal_dns(), + linear_table_modified( + &before.internal_dns_version, + &diff.after_meta.internal_dns_version, + ), + ]); + } else { + builder.push_record(vec![ + format!("{UNCHANGED_PREFIX}{METADATA_DIFF_INDENT}"), + metadata_table_internal_dns(), + linear_table_unchanged( + &before.internal_dns_version, + ), + ]); + }; + + if before.external_dns_version + != diff.after_meta.external_dns_version + { + builder.push_record(vec![ + format!("{MODIFIED_PREFIX}{METADATA_DIFF_INDENT}"), + metadata_table_external_dns(), + linear_table_modified( + &before.external_dns_version, + &diff.after_meta.external_dns_version, + ), + ]); + } else { + builder.push_record(vec![ + format!("{UNCHANGED_PREFIX}{METADATA_DIFF_INDENT}"), + metadata_table_external_dns(), + linear_table_unchanged( + &before.external_dns_version, + ), + ]); + }; } } - // Since we removed common zones above, anything else exists only in - // b2 and was therefore added. - let zones_added = b2_zones.into_values().collect(); + let mut table = builder.build(); + apply_linear_table_settings(&mut table); + table + } + } + fn add_whole_sled_records( + sled_id: Uuid, + sled_zones: &BlueprintZonesConfig, + kind: WholeSledKind, + section: &mut StSectionBuilder, + ) { + let heading = format!( + "{}{SLED_INDENT}sled {sled_id}: zones at generation {}", + kind.prefix(), + sled_zones.generation, + ); + let prefix = kind.prefix(); + let status = kind.status(); + section.make_subsection(SectionSpacing::Always, heading, |s2| { + // Also add another section for zones. + for zone in &sled_zones.zones { + match status { + Some(status) => { + add_zone_record_with_status( + format!("{prefix}{ZONE_INDENT}"), + zone, + status, + s2, + ); + } + None => { + add_zone_record( + format!("{prefix}{ZONE_INDENT}"), + zone, + s2, + ); + } + } + } + }); + } + + fn add_modified_sled_records( + sled_id: Uuid, + modified: &DiffSledModified, + section: &mut StSectionBuilder, + ) { + let (generation_heading, warning) = if modified.generation_before + != modified.generation_after + { ( - sled_id, - DiffSledCommon { - sled_id, - generation_before: b1sledzones.generation, - generation_after: b2sledzones.generation, - zones_added, - zones_removed, - zones_common, - }, + format!( + "zones at generation: {} -> {}", + modified.generation_before, modified.generation_after, + ), + None, ) - }) + } else { + // Modified sleds should always see a generation bump. + ( + format!("zones at generation: {}", modified.generation_before), + Some(format!( + "{WARNING_PREFIX}{ZONE_HEAD_INDENT}\ + warning: generation should have changed" + )), + ) + }; + + let sled_heading = + format!("{MODIFIED_PREFIX}{SLED_INDENT}sled {sled_id}: {generation_heading}"); + + section.make_subsection(SectionSpacing::Always, sled_heading, |s2| { + if let Some(warning) = warning { + s2.push_nested_heading(SectionSpacing::Never, warning); + } + + // The order is: + // + // 1. Unchanged + // 2. Removed + // 3. Modified + // 4. Added + // + // The idea behind the order is to (a) group all changes together + // and (b) put changes towards the bottom, so people have to scroll + // back less. + // + // Sleds follow the same order. If you're changing the order here, + // make sure to keep that in sync. + + // First, unchanged zones. + for zone_unchanged in modified.zones_unchanged() { + add_zone_record( + format!("{UNCHANGED_PREFIX}{ZONE_INDENT}"), + &zone_unchanged.zone_before, + s2, + ); + } + + // Then, removed zones. + for zone in modified.zones_removed() { + add_zone_record_with_status( + format!("{REMOVED_PREFIX}{ZONE_INDENT}"), + zone, + REMOVED, + s2, + ); + } + + // Then, modified zones. + for zone_modified in modified.zones_modified() { + add_modified_zone_records(zone_modified, s2); + } + + // Finally, added zones. + for zone in modified.zones_added() { + add_zone_record_with_status( + format!("{ADDED_PREFIX}{ZONE_INDENT}"), + zone, + ADDED, + s2, + ); + } + + // If no rows were pushed, add a row indicating that for this sled. + if s2.is_empty() { + s2.push_nested_heading( + SectionSpacing::Never, + format!( + "{UNCHANGED_PREFIX}{ZONE_HEAD_INDENT}\ + {NO_ZONES_PARENS}" + ), + ); + } + }); } - pub fn sleds_changed( - &'a self, - ) -> impl Iterator)> + '_ { - self.sleds_in_common().filter(|(_, sled_changes)| { - sled_changes.zones_added().next().is_some() - || sled_changes.zones_removed().next().is_some() - || sled_changes.zones_changed().next().is_some() - }) + /// Add a zone record to this section. + /// + /// This is the meat-and-potatoes of the diff display. + fn add_zone_record( + first_column: String, + zone: &BlueprintZoneConfig, + section: &mut StSectionBuilder, + ) { + section.push_record(vec![ + first_column, + zone.config.zone_type.kind().to_string(), + zone.config.id.to_string(), + zone.disposition.to_string(), + zone.config.underlay_address.to_string(), + ]); } - /// Return a struct that can be used to display the diff in a - /// unified `diff(1)`-like format. - pub fn display(&self) -> OmicronZonesDiffDisplay<'_, 'a> { - OmicronZonesDiffDisplay::new(self) + fn add_zone_record_with_status( + first_column: String, + zone: &BlueprintZoneConfig, + status: &str, + section: &mut StSectionBuilder, + ) { + section.push_record(vec![ + first_column, + zone.config.zone_type.kind().to_string(), + zone.config.id.to_string(), + zone.disposition.to_string(), + zone.config.underlay_address.to_string(), + status.to_string(), + ]); } -} -/// Wrapper to allow a [`OmicronZonesDiff`] to be displayed in a unified -/// `diff(1)`-like format. -/// -/// Returned by [`OmicronZonesDiff::display()`]. -#[derive(Clone, Debug)] -#[must_use = "this struct does nothing unless displayed"] -pub struct OmicronZonesDiffDisplay<'diff, 'a> { - diff: &'diff OmicronZonesDiff<'a>, - // TODO: add colorization with a stylesheet -} + /// Add a change table for the zone to the section. + /// + /// For diffs, this contains a table of changes between two zone + /// records. + fn add_modified_zone_records( + modified: &DiffZoneCommon, + section: &mut StSectionBuilder, + ) { + // Negative record for the before. + let before = &modified.zone_before; + let after = &modified.zone_after; + + // Before record. + add_zone_record_with_status( + format!("{REMOVED_PREFIX}{ZONE_INDENT}"), + &before, + MODIFIED, + section, + ); + + let mut what_changed = Vec::new(); + if before.config.zone_type != after.config.zone_type { + what_changed.push(ZONE_TYPE_CONFIG); + } + if before.disposition != after.disposition { + what_changed.push(DISPOSITION); + } + if before.config.underlay_address != after.config.underlay_address { + what_changed.push(UNDERLAY_IP); + } + debug_assert!( + !what_changed.is_empty(), + "at least something should have changed:\n\ + before = {before:#?}\n\ + after = {after:#?}" + ); + + let record = vec![ + format!("{ADDED_PREFIX}{ZONE_INDENT}"), + // First two columns of data are skipped over since they're + // always the same (verified at diff construction time). + format!(" {SUB_NOT_LAST}"), + "".to_string(), + after.disposition.to_string(), + after.config.underlay_address.to_string(), + ]; + section.push_record(record); + + section.push_spanned_row(format!( + "{MODIFIED_PREFIX}{ZONE_INDENT} \ + {SUB_LAST} changed: {}", + what_changed.join(", "), + )); + } -impl<'diff, 'a> OmicronZonesDiffDisplay<'diff, 'a> { - #[inline] - fn new(diff: &'diff OmicronZonesDiff<'a>) -> Self { - Self { diff } + #[derive(Copy, Clone, Debug)] + enum WholeSledKind { + Removed, + Added, + Unchanged, } - fn print_whole_sled( - &self, - f: &mut fmt::Formatter<'_>, - prefix: char, - label: &str, - bbsledzones: &BlueprintZonesConfig, - sled_id: Uuid, - ) -> fmt::Result { - writeln!(f, "{} sled {} ({})", prefix, sled_id, label)?; - writeln!( - f, - "{} zone config generation {}", - prefix, bbsledzones.generation - )?; - for z in &bbsledzones.zones { - writeln!(f, "{prefix} {} ({label})", z.display())?; + impl WholeSledKind { + fn prefix(self) -> char { + match self { + WholeSledKind::Removed => REMOVED_PREFIX, + WholeSledKind::Added => ADDED_PREFIX, + WholeSledKind::Unchanged => UNCHANGED_PREFIX, + } } - Ok(()) + fn status(self) -> Option<&'static str> { + match self { + WholeSledKind::Removed => Some(REMOVED), + WholeSledKind::Added => Some(ADDED), + WholeSledKind::Unchanged => None, + } + } } -} -impl<'diff, 'a> fmt::Display for OmicronZonesDiffDisplay<'diff, 'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let diff = self.diff; - writeln!(f, "diff {} {}", diff.before_label, diff.after_label)?; - writeln!(f, "--- {}", diff.before_label)?; - writeln!(f, "+++ {}", diff.after_label)?; + // Apply settings for a table which has top-to-bottom rows, and a first + // column with indents. + fn apply_linear_table_settings(table: &mut Table) { + table.with(Style::empty()).with(Padding::zero()).with( + Modify::new(Columns::single(1)) + // Add an padding on the right of the label column to make the + // table visually distinctive. + .with(Padding::new(0, 2, 0, 0)), + ); + } - for (sled_id, sled_zones) in diff.sleds_removed() { - self.print_whole_sled(f, '-', "removed", sled_zones, sled_id)?; - } + // --- + // Heading and other definitions + // --- - for (sled_id, sled_changes) in diff.sleds_in_common() { - // Print a line about the sled itself and zone config generation, - // regardless of whether anything has changed. - writeln!(f, " sled {}", sled_id)?; - if sled_changes.generation_before != sled_changes.generation_after { - writeln!( - f, - "- zone config generation {}", - sled_changes.generation_before - )?; - writeln!( - f, - "+ zone config generation {}", - sled_changes.generation_after - )?; - } else { - writeln!( - f, - " zone config generation {}", - sled_changes.generation_before - )?; - } + // This aligns the heading with the first column of actual text. + const H1_INDENT: &str = " "; + const SLED_HEAD_INDENT: &str = " "; + const SLED_INDENT: &str = " "; + const ZONE_HEAD_INDENT: &str = " "; + // Due to somewhat mysterious reasons with how padding works with tabled, + // this needs to be 3 columns wide rather than 4. + const ZONE_INDENT: &str = " "; + const METADATA_INDENT: &str = " "; + const METADATA_DIFF_INDENT: &str = " "; + + const ADDED_PREFIX: char = '+'; + const REMOVED_PREFIX: char = '-'; + const MODIFIED_PREFIX: char = '*'; + const UNCHANGED_PREFIX: char = ' '; + const WARNING_PREFIX: char = '!'; + + const ARROW: &str = "->"; + const SUB_NOT_LAST: &str = "├─"; + const SUB_LAST: &str = "└─"; + + const ZONE_TYPE: &str = "zone type"; + const ZONE_ID: &str = "zone ID"; + const DISPOSITION: &str = "disposition"; + const UNDERLAY_IP: &str = "underlay IP"; + const ZONE_TYPE_CONFIG: &str = "zone type config"; + const STATUS: &str = "status"; + const REMOVED_SLEDS_HEADING: &str = "REMOVED SLEDS"; + const MODIFIED_SLEDS_HEADING: &str = "MODIFIED SLEDS"; + const UNCHANGED_SLEDS_HEADING: &str = "UNCHANGED SLEDS"; + const ADDED_SLEDS_HEADING: &str = "ADDED SLEDS"; + const REMOVED: &str = "removed"; + const ADDED: &str = "added"; + const MODIFIED: &str = "modified"; + + const METADATA_HEADING: &str = "METADATA"; + const CREATED_BY: &str = "created by"; + const CREATED_AT: &str = "created at"; + const INTERNAL_DNS_VERSION: &str = "internal DNS version"; + const EXTERNAL_DNS_VERSION: &str = "external DNS version"; + const COMMENT: &str = "comment"; + + const UNCHANGED_PARENS: &str = "(unchanged)"; + const NO_ZONES_PARENS: &str = "(no zones)"; + const NONE_PARENS: &str = "(none)"; + const NOT_PRESENT_IN_COLLECTION_PARENS: &str = + "(not present in collection)"; + + fn header_row() -> Vec { + vec![ + // First column is so that the header border aligns with the ZONE + // TABLE section header. + SLED_INDENT.to_string(), + ZONE_TYPE.to_string(), + ZONE_ID.to_string(), + DISPOSITION.to_string(), + UNDERLAY_IP.to_string(), + ] + } - for zone in sled_changes.zones_removed() { - writeln!(f, "- {} (removed)", zone.display())?; - } + fn diff_header_row() -> Vec { + vec![ + // First column is so that the header border aligns with the ZONE + // TABLE section header. + SLED_HEAD_INDENT.to_string(), + ZONE_TYPE.to_string(), + ZONE_ID.to_string(), + DISPOSITION.to_string(), + UNDERLAY_IP.to_string(), + STATUS.to_string(), + ] + } - for zone_changes in sled_changes.zones_in_common() { - if zone_changes.config_changed() { - writeln!( - f, - "- {} (changed)", - zone_changes.zone_before.display(), - )?; - writeln!( - f, - "+ {} (changed)", - zone_changes.zone_after.display(), - )?; - } else if zone_changes.disposition_changed() { - writeln!( - f, - "- {} (disposition changed)", - zone_changes.zone_before.display(), - )?; - writeln!( - f, - "+ {} (disposition changed)", - zone_changes.zone_after.display(), - )?; - } else { - writeln!( - f, - " {} (unchanged)", - zone_changes.zone_before.display(), - )?; - } - } + pub(super) fn metadata_heading() -> String { + format!("{METADATA_HEADING}:") + } - for zone in sled_changes.zones_added() { - writeln!(f, "+ {} (added)", zone.display())?; - } - } + pub(super) fn metadata_diff_heading() -> String { + format!("{H1_INDENT}{METADATA_HEADING}:") + } - for (sled_id, sled_zones) in diff.sleds_added() { - self.print_whole_sled(f, '+', "added", sled_zones, sled_id)?; - } + fn sleds_heading(prefix: char, heading: &'static str) -> String { + format!("{prefix}{SLED_HEAD_INDENT}{heading}:") + } - Ok(()) + fn removed_sleds_heading() -> String { + sleds_heading(UNCHANGED_PREFIX, REMOVED_SLEDS_HEADING) } -} -/// Encapsulates Reconfigurator state -/// -/// This serialized from is intended for saving state from hand-constructed or -/// real, deployed systems and loading it back into a simulator or test suite -/// -/// **This format is not stable. It may change at any time without -/// backwards-compatibility guarantees.** -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UnstableReconfiguratorState { - pub policy: Policy, - pub collections: Vec, - pub blueprints: Vec, - pub internal_dns: BTreeMap, - pub external_dns: BTreeMap, - pub silo_names: Vec, - pub external_dns_zone_names: Vec, + fn added_sleds_heading() -> String { + sleds_heading(UNCHANGED_PREFIX, ADDED_SLEDS_HEADING) + } + + fn modified_sleds_heading() -> String { + sleds_heading(UNCHANGED_PREFIX, MODIFIED_SLEDS_HEADING) + } + + fn unchanged_sleds_heading() -> String { + sleds_heading(UNCHANGED_PREFIX, UNCHANGED_SLEDS_HEADING) + } + + fn metadata_table_internal_dns() -> String { + linear_table_label(&INTERNAL_DNS_VERSION) + } + + fn metadata_table_external_dns() -> String { + linear_table_label(&EXTERNAL_DNS_VERSION) + } + + fn linear_table_label(value: &dyn fmt::Display) -> String { + format!("{value}:") + } + + fn linear_table_modified( + before: &dyn fmt::Display, + after: &dyn fmt::Display, + ) -> String { + format!("{before} {ARROW} {after}") + } + + fn linear_table_unchanged(value: &dyn fmt::Display) -> String { + format!("{value} {UNCHANGED_PARENS}") + } } diff --git a/nexus/types/src/lib.rs b/nexus/types/src/lib.rs index 494573e834..b6286c3f64 100644 --- a/nexus/types/src/lib.rs +++ b/nexus/types/src/lib.rs @@ -34,3 +34,4 @@ pub mod external_api; pub mod identity; pub mod internal_api; pub mod inventory; +mod sectioned_table; diff --git a/nexus/types/src/sectioned_table.rs b/nexus/types/src/sectioned_table.rs new file mode 100644 index 0000000000..addb4c876e --- /dev/null +++ b/nexus/types/src/sectioned_table.rs @@ -0,0 +1,357 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Support for tables with builtin sections. +//! +//! This could live in its own crate (within omicron, or even on crates.io), +//! but is here for now. + +use std::collections::HashSet; +use std::iter; + +use tabled::builder::Builder; +use tabled::grid::config::Border; +use tabled::settings::object::Columns; +use tabled::settings::object::Object; +use tabled::settings::object::Rows; +use tabled::settings::span::ColumnSpan; +use tabled::settings::Modify; +use tabled::settings::Padding; +use tabled::settings::Style; +use tabled::Table; + +/// A sectioned table. +/// +/// A sectioned table allows sections and subsections to be defined, with each +/// section having a title and a list of rows in that section. The section +/// headers and other rows can break standard table conventions. +/// +/// There are two kinds of special rows: +/// +/// 1. Headings: rows that span all columns. +/// 2. Spanned rows: also rows that span all columns, but not as headings. +/// +/// This builder does not currently automatically indent sections or records -- +/// that can be done in the future, though it has to be done with some care. +#[derive(Debug)] +pub(crate) struct StBuilder { + builder: Builder, + // Rows that are marked off with ---- on both sides. + header_rows: Vec, + // Heading rows that span all columns. + headings: Vec<(HeadingSpacing, usize)>, + // Other rows that span all columns. + spanned_rows: Vec, +} + +impl StBuilder { + pub(crate) fn new() -> Self { + let builder = Builder::new(); + + Self { + builder, + header_rows: Vec::new(), + headings: Vec::new(), + spanned_rows: Vec::new(), + } + } + + /// Adds a header row to the table. + /// + /// This row contains column titles, along with *two* initial columns of + /// padding. The border will extend to the first column but not the second + /// one. + pub(crate) fn push_header_row(&mut self, row: Vec) { + self.header_rows.push(self.builder.count_records()); + self.push_record(row); + } + + /// Adds a record to the table. + pub(crate) fn push_record(&mut self, row: Vec) { + self.builder.push_record(row); + } + + /// Makes a new section of the table. + /// + /// This section will not be added to the table unless at least one row is + /// added to it, either directly or via nested sections. + pub(crate) fn make_section( + &mut self, + spacing: SectionSpacing, + heading: String, + cb: impl FnOnce(&mut StSectionBuilder), + ) { + let mut section = StSectionBuilder::from_builder( + self, + spacing.resolve(self.headings.is_empty()), + heading, + ); + cb(&mut section); + section.finish_with_root(self); + } + + /// Does the final build to produce a [`Table`]. + pub(crate) fn build(mut self) -> Table { + // Insert a column between 0 and 1 to enable header borders to be + // properly aligned with the rest of the text. + self.builder.insert_column( + 1, + iter::repeat("").take(self.builder.count_records()), + ); + + let mut table = self.builder.build(); + table + .with(Style::blank()) + .with( + // Columns 0 and 1 (indent/gutter) should not have any border + // and padding. + Modify::new(Columns::new(0..=1)) + .with(Border::empty()) + .with(Padding::zero()), + ) + .with( + Modify::new(Columns::single(2)) + // Column 2 (first column of actual data) should not have + // left padding. + .with(Padding::new(0, 1, 0, 0)), + ) + .with( + Modify::new(Columns::last()) + // Rightmost column should have no border and padding. + .with(Border::empty()) + .with(Padding::zero()), + ); + apply_normal_row_settings( + &mut table, + self.header_rows + .iter() + .copied() + .chain(self.headings.iter().map(|(_, i)| *i)) + .chain(self.spanned_rows.iter().copied()) + .collect(), + ); + apply_header_row_settings(&mut table, &self.header_rows); + apply_heading_settings(&mut table, &self.headings); + apply_spanned_row_settings(&mut table, &self.spanned_rows); + + table + } +} + +/// A part of a sectioned table. +/// +/// Created by [`StBuilder::make_section`] or +/// [`StNestedBuilder::make_subsection`]. +#[derive(Debug)] +pub(crate) struct StSectionBuilder { + start_index: usize, + spacing: HeadingSpacing, + heading: String, + rows: Vec>, + // Indexes for special rows, stored as absolute indexes wrt the overall + // zone table (i.e. start_index + 1 + index in rows). + nested_headings: Vec<(HeadingSpacing, usize)>, + spanned_rows: Vec, +} + +impl StSectionBuilder { + fn from_builder( + builder: &StBuilder, + spacing: HeadingSpacing, + heading: String, + ) -> Self { + let start_index = builder.builder.count_records(); + Self { + start_index, + spacing, + heading, + rows: Vec::new(), + nested_headings: Vec::new(), + spanned_rows: Vec::new(), + } + } + + pub(crate) fn is_empty(&self) -> bool { + self.rows.is_empty() + } + + pub(crate) fn push_record(&mut self, row: Vec) { + self.rows.push(row); + } + + pub(crate) fn push_spanned_row(&mut self, row: String) { + self.spanned_rows.push(self.next_row()); + self.rows.push(vec![row]); + } + + pub(crate) fn push_nested_heading( + &mut self, + spacing: SectionSpacing, + heading: String, + ) { + self.nested_headings.push(( + spacing.resolve(self.nested_headings.is_empty()), + self.next_row(), + )); + self.rows.push(vec![heading]); + } + + /// Makes a new subsection of this section. + /// + /// This subsection will not be added to the table unless at least one row + /// is added to it, either directly or via nested sections. + pub(crate) fn make_subsection( + &mut self, + spacing: SectionSpacing, + heading: String, + cb: impl FnOnce(&mut Self), + ) { + let mut subsection = Self { + start_index: self.next_row(), + spacing: spacing.resolve(self.nested_headings.is_empty()), + heading, + rows: Vec::new(), + nested_headings: Vec::new(), + spanned_rows: Vec::new(), + }; + cb(&mut subsection); + subsection.finish_with_parent(self); + } + + fn next_row(&self) -> usize { + // +1 to account for the heading row. + self.start_index + 1 + self.rows.len() + } + + fn finish_with_root(self, root: &mut StBuilder) { + if !self.rows.is_empty() { + // Push all the indexes. + root.headings.push((self.spacing, self.start_index)); + root.headings.extend(self.nested_headings); + root.spanned_rows.extend(self.spanned_rows); + + // Push all the rows. + root.push_record(vec![self.heading]); + for row in self.rows { + root.push_record(row); + } + } + } + + fn finish_with_parent(self, parent: &mut StSectionBuilder) { + if !self.rows.is_empty() { + // Push all the indexes. + parent.nested_headings.push((self.spacing, self.start_index)); + parent.nested_headings.extend(self.nested_headings); + parent.spanned_rows.extend(self.spanned_rows); + + // Push all the rows. + parent.rows.push(vec![self.heading]); + parent.rows.extend(self.rows); + } + } +} + +/// Spacing for sections. +#[derive(Copy, Clone, Debug)] +pub(crate) enum SectionSpacing { + /// Always add a line of spacing above the section heading. + /// + /// There will always be one row of padding above the heading. + Always, + + /// Only add a line of spacing if this isn't the first heading in the + /// series. + IfNotFirst, + + /// Do not add a line of spacing above the heading. + Never, +} + +impl SectionSpacing { + fn resolve(self, is_empty: bool) -> HeadingSpacing { + match (self, is_empty) { + (SectionSpacing::Always, _) => HeadingSpacing::Yes, + (SectionSpacing::IfNotFirst, true) => HeadingSpacing::No, + (SectionSpacing::IfNotFirst, false) => HeadingSpacing::Yes, + (SectionSpacing::Never, _) => HeadingSpacing::No, + } + } +} + +/// Spacing for headings -- a resolved form of [`SectionSpacing`]. +#[derive(Copy, Clone, Debug)] +enum HeadingSpacing { + /// Add a line of padding above the heading. + Yes, + + /// Do not add a line of padding above the heading. + No, +} + +fn apply_normal_row_settings(table: &mut Table, special_rows: HashSet) { + for row in 0..table.count_rows() { + if special_rows.contains(&row) { + continue; + } + + table.with( + Modify::new((row, 0)) + // Adjust the first column to span 2 (the extra indent). + .with(ColumnSpan::new(2)), + ); + } +} + +fn apply_header_row_settings(table: &mut Table, header_rows: &[usize]) { + for &hr in header_rows { + table.with( + Modify::new(Rows::single(hr).intersect(Columns::new(1..))) + // Column 1 onwards (everything after the initial indent) have + // borders. + .with(Border::new( + // top/bottom + Some('-'), + Some('-'), + // no left/right + None, + None, + // corners + Some('-'), + Some('-'), + Some('-'), + Some('-'), + )), + ); + } +} + +fn apply_heading_settings( + table: &mut Table, + headings: &[(HeadingSpacing, usize)], +) { + for &(kind, h) in headings { + let padding = match kind { + HeadingSpacing::Yes => Padding::new(0, 0, 1, 0), + HeadingSpacing::No => Padding::new(0, 0, 0, 0), + }; + + table.with( + Modify::new((h, 0)) + // Adjust each heading row to span the whole row. + .with(ColumnSpan::max()) + .with(padding), + ); + } +} + +fn apply_spanned_row_settings(table: &mut Table, spanned_rows: &[usize]) { + for &sr in spanned_rows { + table.with( + Modify::new((sr, 0)) + // Adjust each spanned row to span the whole row. + .with(ColumnSpan::max()), + ); + } +}