From e5094dceedd7bb00df307650de50e365f128d041 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 28 Mar 2024 18:59:14 -0700 Subject: [PATCH] [reconfigurator] use tabled to display blueprints and diffs (#5270) While developing #5238, I noticed that the output was getting significantly busier and less aligned. I decided to prototype out using `tabled` to display outputs, and I really liked the results. Examples that cover all of the cases are included in the PR. In the future I'd also like to add color support on the CLI, and expand it to inventory and `omdb` (it's similar except it doesn't have the zone policy table). Some other changes that are bundled into this PR: * Sort by (zone type, zone ID) rather than zone ID, to keep zones of the same type grouped together. * Moved unchanged data to the top to allow users to see less scrollback. * Moved metadata to the bottom for the same reason. * Add information about the zone config being changed. * Change `Blueprint::diff_sleds` and `Blueprint::diff_sleds_from_collection` to `Blueprint::diff_since_blueprint` and `diff_since_collection` recently. * Reordered `diff_since_blueprint`'s arguments so that `self` is after and the argument is before, to align with `diff_since_collection`. (I found that surprising!) * Renamed the diff type from `OmicronZonesDiff` to `BlueprintDiff`, since it's going to contain a lot more than zones. * Return an error from the diff methods, specifically if the before and after have the same zone ID but different types. Depends on #5238 and #5341. --- Cargo.lock | 1 + clients/sled-agent-client/src/lib.rs | 71 +- dev-tools/omdb/src/bin/omdb/db.rs | 2 +- dev-tools/omdb/src/bin/omdb/nexus.rs | 3 +- dev-tools/reconfigurator-cli/src/main.rs | 10 +- .../db-queries/src/db/datastore/deployment.rs | 11 +- nexus/inventory/src/collector.rs | 2 +- nexus/reconfigurator/execution/src/dns.rs | 2 +- .../planning/src/blueprint_builder.rs | 42 +- nexus/reconfigurator/planning/src/planner.rs | 183 ++- .../output/blueprint_builder_initial_diff.txt | 54 + .../output/planner_basic_add_sled_2_3.txt | 107 +- .../output/planner_basic_add_sled_3_5.txt | 128 +- .../output/planner_nonprovisionable_1_2.txt | 181 ++- .../output/planner_nonprovisionable_2_2a.txt | 104 ++ .../output/planner_nonprovisionable_bp2.txt | 94 ++ nexus/types/Cargo.toml | 1 + nexus/types/src/deployment.rs | 1381 +++++++++++++---- nexus/types/src/lib.rs | 1 + nexus/types/src/sectioned_table.rs | 357 +++++ 20 files changed, 2123 insertions(+), 612 deletions(-) create mode 100644 nexus/reconfigurator/planning/tests/output/blueprint_builder_initial_diff.txt create mode 100644 nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_2_2a.txt create mode 100644 nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_bp2.txt create mode 100644 nexus/types/src/sectioned_table.rs 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()), + ); + } +}