From 5b26fc45acf59b7ba186e88f9799d2e24d4fbfb6 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 12 Nov 2025 11:11:27 -0500 Subject: [PATCH 01/24] builder: add method to add boundary NTP directly instead of always promoting internal NTP --- .../planning/src/blueprint_builder/builder.rs | 180 +++++++++++++----- 1 file changed, 132 insertions(+), 48 deletions(-) diff --git a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs index 06e438fb3e1..5455805d804 100644 --- a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs +++ b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs @@ -1813,30 +1813,45 @@ impl<'a> BlueprintBuilder<'a> { self.sled_add_zone(sled_id, zone) } - pub fn sled_promote_internal_ntp_to_boundary_ntp( - &mut self, - sled_id: SledUuid, - image_source: BlueprintZoneImageSource, - external_ip: ExternalSnatNetworkingChoice, - ) -> Result<(), Error> { - // The upstream NTP/DNS servers and domain _should_ come from Nexus and - // be modifiable by the operator, but currently can only be set at RSS. - // We can only promote a new boundary NTP zone by copying these settings - // from an existing one. - let (ntp_servers, dns_servers, domain) = self - .parent_blueprint + // The upstream NTP/DNS servers and domain _should_ come from Nexus and be + // modifiable by the operator, but currently can only be set at RSS. We can + // only promote a new boundary NTP zone by copying these settings from an + // existing one. + fn infer_boundary_ntp_config_from_parent_blueprint( + &self, + ) -> Result { + self.parent_blueprint .all_omicron_zones(BlueprintZoneDisposition::any) .find_map(|(_, z)| match &z.zone_type { - BlueprintZoneType::BoundaryNtp(zone_config) => Some(( - zone_config.ntp_servers.clone(), - zone_config.dns_servers.clone(), - zone_config.domain.clone(), - )), + BlueprintZoneType::BoundaryNtp(zone_config) => { + Some(BoundaryNtpConfig { + ntp_servers: zone_config.ntp_servers.clone(), + dns_servers: zone_config.dns_servers.clone(), + domain: zone_config.domain.clone(), + }) + } _ => None, }) - .ok_or(Error::NoBoundaryNtpZonesInParentBlueprint)?; + .ok_or(Error::NoBoundaryNtpZonesInParentBlueprint) + } - self.sled_promote_internal_ntp_to_boundary_ntp_with_config( + /// Add a new boundary NTP server to a sled. + /// + /// This is unusual: typically during planning we promote internal NTP + /// servers to boundary NTP servers via + /// `sled_promote_internal_ntp_to_boundary_ntp()`, because adding a new + /// boundary NTP zone to a sled is only valid if the sled doesn't currently + /// have any NTP zone at all. Only tests and possibly RSS can really make + /// use of this. + pub fn sled_add_zone_boundary_ntp( + &mut self, + sled_id: SledUuid, + image_source: BlueprintZoneImageSource, + external_ip: ExternalSnatNetworkingChoice, + ) -> Result<(), Error> { + let BoundaryNtpConfig { ntp_servers, dns_servers, domain } = + self.infer_boundary_ntp_config_from_parent_blueprint()?; + self.sled_add_zone_boundary_ntp_with_config( sled_id, ntp_servers, dns_servers, @@ -1846,7 +1861,15 @@ impl<'a> BlueprintBuilder<'a> { ) } - pub fn sled_promote_internal_ntp_to_boundary_ntp_with_config( + /// Add a new boundary NTP server to a sled. + /// + /// This is unusual: typically during planning we promote internal NTP + /// servers to boundary NTP servers via + /// `sled_promote_internal_ntp_to_boundary_ntp()`, because adding a new + /// boundary NTP zone to a sled is only valid if the sled doesn't currently + /// have any NTP zone at all. Only tests and possibly RSS can really make + /// use of this. + pub fn sled_add_zone_boundary_ntp_with_config( &mut self, sled_id: SledUuid, ntp_servers: Vec, @@ -1861,38 +1884,16 @@ impl<'a> BlueprintBuilder<'a> { )) })?; - // Find the internal NTP zone and expunge it. - let mut internal_ntp_zone_id_iter = editor + // Ensure we have no other in-service NTP zones. + if let Some(in_service_ntp_zone) = editor .zones(BlueprintZoneDisposition::is_in_service) - .filter_map(|zone| { - if matches!(zone.zone_type, BlueprintZoneType::InternalNtp(_)) { - Some(zone.id) - } else { - None - } - }); - - // We should have exactly one internal NTP zone. - let internal_ntp_zone_id = - internal_ntp_zone_id_iter.next().ok_or_else(|| { - Error::Planner(anyhow!( - "cannot promote internal NTP zone on sled {sled_id}: \ - no internal NTP zone found" - )) - })?; - if internal_ntp_zone_id_iter.next().is_some() { + .find(|zone| zone.zone_type.is_ntp()) + { return Err(Error::Planner(anyhow!( - "sled {sled_id} has multiple internal NTP zones" + "attempted to add boundary NTP zone to sled {sled_id} which \ + already has an in-service NTP zone: {in_service_ntp_zone:?}" ))); } - std::mem::drop(internal_ntp_zone_id_iter); - - // Expunge the internal NTP zone. - editor.expunge_zone(&internal_ntp_zone_id).map_err(|error| { - Error::Planner(anyhow!(error).context(format!( - "error expunging internal NTP zone from sled {sled_id}" - ))) - })?; // Add the new boundary NTP zone. let new_zone_id = self.rng.sled_rng(sled_id).next_zone(); @@ -1947,6 +1948,83 @@ impl<'a> BlueprintBuilder<'a> { ) } + pub fn sled_promote_internal_ntp_to_boundary_ntp( + &mut self, + sled_id: SledUuid, + image_source: BlueprintZoneImageSource, + external_ip: ExternalSnatNetworkingChoice, + ) -> Result<(), Error> { + let BoundaryNtpConfig { ntp_servers, dns_servers, domain } = + self.infer_boundary_ntp_config_from_parent_blueprint()?; + self.sled_promote_internal_ntp_to_boundary_ntp_with_config( + sled_id, + ntp_servers, + dns_servers, + domain, + image_source, + external_ip, + ) + } + + pub fn sled_promote_internal_ntp_to_boundary_ntp_with_config( + &mut self, + sled_id: SledUuid, + ntp_servers: Vec, + dns_servers: Vec, + domain: Option, + image_source: BlueprintZoneImageSource, + external_ip: ExternalSnatNetworkingChoice, + ) -> Result<(), Error> { + let editor = self.sled_editors.get_mut(&sled_id).ok_or_else(|| { + Error::Planner(anyhow!( + "tried to promote NTP zone on unknown sled {sled_id}" + )) + })?; + + // Find the internal NTP zone and expunge it. + let mut internal_ntp_zone_id_iter = editor + .zones(BlueprintZoneDisposition::is_in_service) + .filter_map(|zone| { + if matches!(zone.zone_type, BlueprintZoneType::InternalNtp(_)) { + Some(zone.id) + } else { + None + } + }); + + // We should have exactly one internal NTP zone. + let internal_ntp_zone_id = + internal_ntp_zone_id_iter.next().ok_or_else(|| { + Error::Planner(anyhow!( + "cannot promote internal NTP zone on sled {sled_id}: \ + no internal NTP zone found" + )) + })?; + if internal_ntp_zone_id_iter.next().is_some() { + return Err(Error::Planner(anyhow!( + "sled {sled_id} has multiple internal NTP zones" + ))); + } + std::mem::drop(internal_ntp_zone_id_iter); + + // Expunge the internal NTP zone. + editor.expunge_zone(&internal_ntp_zone_id).map_err(|error| { + Error::Planner(anyhow!(error).context(format!( + "error expunging internal NTP zone from sled {sled_id}" + ))) + })?; + + // Add the new boundary NTP zone. + self.sled_add_zone_boundary_ntp_with_config( + sled_id, + ntp_servers, + dns_servers, + domain, + image_source, + external_ip, + ) + } + pub fn sled_expunge_zone( &mut self, sled_id: SledUuid, @@ -2542,6 +2620,12 @@ impl fmt::Display for BpMupdateOverrideNotClearedReason { } } +struct BoundaryNtpConfig { + ntp_servers: Vec, + dns_servers: Vec, + domain: Option, +} + #[cfg(test)] pub mod test { use super::*; From 5cef8ac337db00e08ea0ec114ea1a795158f37b2 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 12 Nov 2025 11:15:17 -0500 Subject: [PATCH 02/24] use builder in rack_set_initialized_with_services() --- nexus/db-queries/src/db/datastore/rack.rs | 381 ++++++++---------- .../allocators/external_networking.rs | 3 +- 2 files changed, 163 insertions(+), 221 deletions(-) diff --git a/nexus/db-queries/src/db/datastore/rack.rs b/nexus/db-queries/src/db/datastore/rack.rs index 92854041b6b..ce8eef59a43 100644 --- a/nexus/db-queries/src/db/datastore/rack.rs +++ b/nexus/db-queries/src/db/datastore/rack.rs @@ -1067,6 +1067,9 @@ mod test { use nexus_config::NUM_INITIAL_RESERVED_IP_ADDRESSES; use nexus_db_model::{DnsGroup, Generation, InitialDnsGroup}; use nexus_inventory::now_db_precision; + use nexus_reconfigurator_planning::blueprint_builder::BlueprintBuilder; + use nexus_reconfigurator_planning::blueprint_editor::ExternalNetworkingAllocator; + use nexus_reconfigurator_planning::planner::PlannerRng; use nexus_reconfigurator_planning::system::{ SledBuilder, SystemDescription, }; @@ -1077,13 +1080,13 @@ mod test { use nexus_types::deployment::CockroachDbPreserveDowngrade; use nexus_types::deployment::ExternalIpPolicy; use nexus_types::deployment::PendingMgsUpdates; + use nexus_types::deployment::SledFilter; use nexus_types::deployment::{ BlueprintZoneConfig, OmicronZoneExternalFloatingAddr, OmicronZoneExternalFloatingIp, }; use nexus_types::deployment::{ - BlueprintZoneDisposition, BlueprintZoneImageSource, - OmicronZoneExternalSnatIp, OximeterReadMode, + BlueprintZoneDisposition, BlueprintZoneImageSource, OximeterReadMode, }; use nexus_types::external_api::shared::SiloIdentityMode; use nexus_types::external_api::views::SledState; @@ -1093,13 +1096,12 @@ mod test { use nexus_types::inventory::NetworkInterfaceKind; use omicron_common::address::NEXUS_OPTE_IPV6_SUBNET; use omicron_common::address::{ - DNS_OPTE_IPV4_SUBNET, NEXUS_OPTE_IPV4_SUBNET, NTP_OPTE_IPV4_SUBNET, + DNS_OPTE_IPV4_SUBNET, NEXUS_OPTE_IPV4_SUBNET, }; use omicron_common::api::external::http_pagination::PaginatedBy; use omicron_common::api::external::{ IdentityMetadataCreateParams, MacAddr, Vni, }; - use omicron_common::api::internal::shared::SourceNatConfig; use omicron_common::zpool_name::ZpoolName; use omicron_test_utils::dev; use omicron_uuid_kinds::BlueprintUuid; @@ -1417,8 +1419,16 @@ mod test { Ipv4Addr::new(1, 2, 3, 6), )) .unwrap(); - let external_ip_policy = - ExternalIpPolicy::single_pool_no_external_dns(service_ip_pool); + let external_ip_policy = { + let mut builder = ExternalIpPolicy::builder(); + builder + .push_service_pool_range(service_ip_pool) + .expect("valid pool"); + builder + .add_external_dns_ip("1.2.3.4".parse().unwrap()) + .expect("valid IP"); + builder.build() + }; let mut system = SystemDescription::new(); system @@ -1429,217 +1439,150 @@ mod test { .expect("failed to add sled2") .sled(SledBuilder::new().id(sled3.id())) .expect("failed to add sled3"); + let planning_input = system + .to_planning_input_builder() + .expect("created planning input builder") + .build(); + let empty_starting_blueprint = BlueprintBuilder::build_empty_with_sleds( + std::iter::empty(), + test_name, + ); + let mut external_networking_alloc = + ExternalNetworkingAllocator::from_blueprint( + &empty_starting_blueprint, + planning_input.external_ip_policy(), + ) + .expect("constructed allocator"); + + let mut builder = BlueprintBuilder::new_based_on( + &opctx.log, + &empty_starting_blueprint, + &planning_input, + test_name, + PlannerRng::from_entropy(), + ) + .expect("created blueprint builder"); + for (sled_id, sled_resources) in + planning_input.all_sled_resources(SledFilter::InService) + { + builder + .sled_add_disks(sled_id, &sled_resources) + .expect("added disks"); + } - let external_dns_ip = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)); - let external_dns_pip = DNS_OPTE_IPV4_SUBNET - .nth(NUM_INITIAL_RESERVED_IP_ADDRESSES + 1) - .unwrap(); - let external_dns_id = OmicronZoneUuid::new_v4(); - let nexus_ip = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 6)); - let nexus_pip = NEXUS_OPTE_IPV4_SUBNET - .nth(NUM_INITIAL_RESERVED_IP_ADDRESSES + 1) - .unwrap(); - let nexus_id = OmicronZoneUuid::new_v4(); - let ntp1_ip = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 5)); - let ntp1_pip = NTP_OPTE_IPV4_SUBNET - .nth(NUM_INITIAL_RESERVED_IP_ADDRESSES + 1) - .unwrap(); - let ntp1_id = OmicronZoneUuid::new_v4(); - let ntp2_ip = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 5)); - let ntp2_pip = NTP_OPTE_IPV4_SUBNET - .nth(NUM_INITIAL_RESERVED_IP_ADDRESSES + 2) - .unwrap(); - let ntp2_id = OmicronZoneUuid::new_v4(); - let ntp3_id = OmicronZoneUuid::new_v4(); - let mut macs = MacAddr::iter_system(); + let external_dns_networking = external_networking_alloc + .for_new_external_dns() + .expect("got IP for external DNS"); + let external_dns_ip = external_dns_networking.external_ip; + + let nexus_networking = external_networking_alloc + .for_new_nexus() + .expect("got IP for Nexus"); + let nexus_ip = nexus_networking.external_ip; + + let ntp1_networking = external_networking_alloc + .for_new_boundary_ntp() + .expect("got IP for boundary NTP"); + let ntp2_networking = external_networking_alloc + .for_new_boundary_ntp() + .expect("got IP for boundary NTP"); + let ntp1_ip = ntp1_networking.snat_cfg.ip; + let ntp2_ip = ntp2_networking.snat_cfg.ip; + + // Add specific zones: + // + // * Sled 1 gets external DNS and boundary NTP + // * Sled 2 gets Nexus and boundary NTP + // * Sled 3 gets internal NTP + builder + .sled_add_zone_external_dns( + sled1.id(), + BlueprintZoneImageSource::InstallDataset, + external_dns_networking, + ) + .expect("added zone"); + builder + .sled_add_zone_nexus_with_config( + sled2.id(), + false, + Vec::new(), + BlueprintZoneImageSource::InstallDataset, + nexus_networking, + *Generation::new(), + ) + .expect("added zone"); - let mut blueprint_zones = BTreeMap::new(); - let dataset = random_dataset(); - blueprint_zones.insert( - sled1.id(), - [ - BlueprintZoneConfig { - disposition: BlueprintZoneDisposition::InService, - id: external_dns_id, - filesystem_pool: dataset.pool_name, - zone_type: BlueprintZoneType::ExternalDns( - blueprint_zone_type::ExternalDns { - dataset, - http_address: "[::1]:80".parse().unwrap(), - dns_address: OmicronZoneExternalFloatingAddr { - id: ExternalIpUuid::new_v4(), - addr: SocketAddr::new(external_dns_ip, 53), - }, - nic: NetworkInterface { - id: Uuid::new_v4(), - kind: NetworkInterfaceKind::Service { - id: external_dns_id.into_untyped_uuid(), - }, - name: "external-dns".parse().unwrap(), - ip: external_dns_pip.into(), - mac: macs.next().unwrap(), - subnet: IpNet::from(*DNS_OPTE_IPV4_SUBNET), - vni: Vni::SERVICES_VNI, - primary: true, - slot: 0, - transit_ips: vec![], - }, - }, - ), - image_source: BlueprintZoneImageSource::InstallDataset, - }, - BlueprintZoneConfig { - disposition: BlueprintZoneDisposition::InService, - id: ntp1_id, - filesystem_pool: random_zpool(), - zone_type: BlueprintZoneType::BoundaryNtp( - blueprint_zone_type::BoundaryNtp { - address: "[::1]:80".parse().unwrap(), - ntp_servers: vec![], - dns_servers: vec![], - domain: None, - nic: NetworkInterface { - id: Uuid::new_v4(), - kind: NetworkInterfaceKind::Service { - id: ntp1_id.into_untyped_uuid(), - }, - name: "ntp1".parse().unwrap(), - ip: ntp1_pip.into(), - mac: macs.next().unwrap(), - subnet: IpNet::from(*NTP_OPTE_IPV4_SUBNET), - vni: Vni::SERVICES_VNI, - primary: true, - slot: 0, - transit_ips: vec![], - }, - external_ip: OmicronZoneExternalSnatIp { - id: ExternalIpUuid::new_v4(), - snat_cfg: SourceNatConfig::new( - ntp1_ip, 16384, 32767, - ) - .unwrap(), - }, - }, - ), - image_source: BlueprintZoneImageSource::InstallDataset, - }, - ] - .into_iter() - .collect::>(), - ); - blueprint_zones.insert( - sled2.id(), - [ - BlueprintZoneConfig { - disposition: BlueprintZoneDisposition::InService, - id: nexus_id, - filesystem_pool: random_zpool(), - zone_type: BlueprintZoneType::Nexus( - blueprint_zone_type::Nexus { - internal_address: "[::1]:80".parse().unwrap(), - lockstep_port: - omicron_common::address::NEXUS_LOCKSTEP_PORT, - external_ip: OmicronZoneExternalFloatingIp { - id: ExternalIpUuid::new_v4(), - ip: nexus_ip, - }, - external_tls: false, - external_dns_servers: vec![], - nic: NetworkInterface { - id: Uuid::new_v4(), - kind: NetworkInterfaceKind::Service { - id: nexus_id.into_untyped_uuid(), - }, - name: "nexus".parse().unwrap(), - ip: nexus_pip.into(), - mac: macs.next().unwrap(), - subnet: IpNet::from(*NEXUS_OPTE_IPV4_SUBNET), - vni: Vni::SERVICES_VNI, - primary: true, - slot: 0, - transit_ips: vec![], - }, - nexus_generation: *Generation::new(), - }, - ), - image_source: BlueprintZoneImageSource::InstallDataset, - }, - BlueprintZoneConfig { - disposition: BlueprintZoneDisposition::InService, - id: ntp2_id, - filesystem_pool: random_zpool(), - zone_type: BlueprintZoneType::BoundaryNtp( - blueprint_zone_type::BoundaryNtp { - address: "[::1]:80".parse().unwrap(), - ntp_servers: vec![], - dns_servers: vec![], - domain: None, - nic: NetworkInterface { - id: Uuid::new_v4(), - kind: NetworkInterfaceKind::Service { - id: ntp2_id.into_untyped_uuid(), - }, - name: "ntp2".parse().unwrap(), - ip: ntp2_pip.into(), - mac: macs.next().unwrap(), - subnet: IpNet::from(*NTP_OPTE_IPV4_SUBNET), - vni: Vni::SERVICES_VNI, - primary: true, - slot: 0, - transit_ips: vec![], - }, - external_ip: OmicronZoneExternalSnatIp { - id: ExternalIpUuid::new_v4(), - snat_cfg: SourceNatConfig::new( - ntp2_ip, 0, 16383, - ) - .unwrap(), - }, - }, - ), - image_source: BlueprintZoneImageSource::InstallDataset, - }, - ] - .into_iter() - .collect(), - ); - blueprint_zones.insert( - sled3.id(), - [BlueprintZoneConfig { - disposition: BlueprintZoneDisposition::InService, - id: ntp3_id, - filesystem_pool: random_zpool(), - zone_type: BlueprintZoneType::InternalNtp( - blueprint_zone_type::InternalNtp { - address: "[::1]:80".parse().unwrap(), - }, - ), - image_source: BlueprintZoneImageSource::InstallDataset, - }] - .into_iter() - .collect(), - ); - let blueprint_id = BlueprintUuid::new_v4(); - let blueprint = Blueprint { - id: blueprint_id, - sleds: make_sled_config_only_zones(blueprint_zones), - pending_mgs_updates: PendingMgsUpdates::new(), - cockroachdb_setting_preserve_downgrade: - CockroachDbPreserveDowngrade::DoNotModify, - parent_blueprint_id: None, - internal_dns_version: *Generation::new(), - external_dns_version: *Generation::new(), - target_release_minimum_generation: *Generation::new(), - nexus_generation: *Generation::new(), - cockroachdb_fingerprint: String::new(), - clickhouse_cluster_config: None, - oximeter_read_version: *Generation::new(), - oximeter_read_mode: OximeterReadMode::SingleNode, - time_created: now_db_precision(), - creator: "test suite".to_string(), - comment: "test blueprint".to_string(), - source: BlueprintSource::Test, - }; + for (sled_id, external_ip) in + [(sled1.id(), ntp1_networking), (sled2.id(), ntp2_networking)] + { + builder + .sled_add_zone_boundary_ntp_with_config( + sled_id, + Vec::new(), + Vec::new(), + None, + BlueprintZoneImageSource::InstallDataset, + external_ip, + ) + .expect("added boundary NTP"); + } + builder + .sled_ensure_zone_ntp( + sled3.id(), + BlueprintZoneImageSource::InstallDataset, + ) + .expect("ensured internal NTP"); + + let mut blueprint = builder.build(BlueprintSource::Test); + + // We're emulating RSS, which inserts the initial blueprint. Clear out + // the link back to the empty parent we started with. + blueprint.parent_blueprint_id = None; + + // Find the zone IDs of the services we added above. + let mut nexus_id = None; + let mut external_dns_id = None; + let mut ntp1_id = None; + let mut ntp2_id = None; + let mut ntp3_id = None; + for (sled_id, zone) in + blueprint.all_omicron_zones(BlueprintZoneDisposition::is_in_service) + { + match &zone.zone_type { + BlueprintZoneType::BoundaryNtp(_) => { + let which = if sled_id == sled1.id() { + &mut ntp1_id + } else if sled_id == sled2.id() { + &mut ntp2_id + } else { + panic!("unexpected boundary NTP zone") + }; + assert!(which.is_none(), "just 1 NTP zone per sled"); + *which = Some(zone.id); + } + BlueprintZoneType::InternalNtp(_) => { + assert_eq!(sled_id, sled3.id()); + assert!(ntp3_id.is_none(), "just 1 internal NTP zone"); + ntp3_id = Some(zone.id); + } + BlueprintZoneType::ExternalDns(_) => { + assert_eq!(sled_id, sled1.id()); + assert!(external_dns_id.is_none(), "just 1 DNS zone"); + external_dns_id = Some(zone.id); + } + BlueprintZoneType::Nexus(_) => { + assert_eq!(sled_id, sled2.id()); + assert!(nexus_id.is_none(), "just 1 Nexus zone"); + nexus_id = Some(zone.id); + } + _ => (), + } + } + let nexus_id = nexus_id.expect("found Nexus zone"); + let external_dns_id = external_dns_id.expect("found DNS zone"); + let ntp1_id = ntp1_id.expect("found NTP zone 1"); + let ntp2_id = ntp2_id.expect("found NTP zone 2"); + let ntp3_id = ntp3_id.expect("found NTP zone 3"); let rack = datastore .rack_set_initialized( @@ -1697,13 +1640,13 @@ mod test { assert!(ntp1_external_ip.is_service); assert_eq!(ntp1_external_ip.kind, IpKind::SNat); - assert_eq!(ntp1_external_ip.first_port.0, 16384); - assert_eq!(ntp1_external_ip.last_port.0, 32767); + assert_eq!(ntp1_external_ip.first_port.0, 0); + assert_eq!(ntp1_external_ip.last_port.0, 16383); assert!(ntp2_external_ip.is_service); assert_eq!(ntp2_external_ip.kind, IpKind::SNat); - assert_eq!(ntp2_external_ip.first_port.0, 0); - assert_eq!(ntp2_external_ip.last_port.0, 16383); + assert_eq!(ntp2_external_ip.first_port.0, 16384); + assert_eq!(ntp2_external_ip.last_port.0, 32767); // Furthermore, we should be able to see that these IP addresses have // been allocated as a part of a service IP pool. diff --git a/nexus/reconfigurator/planning/src/blueprint_editor/allocators/external_networking.rs b/nexus/reconfigurator/planning/src/blueprint_editor/allocators/external_networking.rs index 53703f6a7ab..979cd1bd453 100644 --- a/nexus/reconfigurator/planning/src/blueprint_editor/allocators/external_networking.rs +++ b/nexus/reconfigurator/planning/src/blueprint_editor/allocators/external_networking.rs @@ -73,8 +73,7 @@ impl ExternalNetworkingAllocator { /// Construct an `ExternalNetworkingAllocator` that hands out IPs based on /// `external_ip_policy`, treating any IPs used by in-service zones /// in `blueprint` as already-in-use. - #[cfg(test)] - pub(crate) fn from_blueprint( + pub fn from_blueprint( blueprint: &nexus_types::deployment::Blueprint, external_ip_policy: &ExternalIpPolicy, ) -> anyhow::Result { From 6baa021df71f9537f241f024421941bcd805de1e Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 12 Nov 2025 11:28:02 -0500 Subject: [PATCH 03/24] rack_set_initialized_missing_service_pool_ip_throws_error() --- nexus/db-queries/src/db/datastore/rack.rs | 168 ++++++++---------- .../planning/src/blueprint_builder/builder.rs | 2 +- 2 files changed, 79 insertions(+), 91 deletions(-) diff --git a/nexus/db-queries/src/db/datastore/rack.rs b/nexus/db-queries/src/db/datastore/rack.rs index ce8eef59a43..81d07cd328e 100644 --- a/nexus/db-queries/src/db/datastore/rack.rs +++ b/nexus/db-queries/src/db/datastore/rack.rs @@ -1069,6 +1069,7 @@ mod test { use nexus_inventory::now_db_precision; use nexus_reconfigurator_planning::blueprint_builder::BlueprintBuilder; use nexus_reconfigurator_planning::blueprint_editor::ExternalNetworkingAllocator; + use nexus_reconfigurator_planning::blueprint_editor::ExternalNetworkingChoice; use nexus_reconfigurator_planning::planner::PlannerRng; use nexus_reconfigurator_planning::system::{ SledBuilder, SystemDescription, @@ -1111,10 +1112,12 @@ mod test { use omicron_uuid_kinds::SledUuid; use omicron_uuid_kinds::ZpoolUuid; use oxnet::IpNet; + use slog::Logger; use std::collections::{BTreeMap, HashMap}; use std::net::Ipv6Addr; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::num::NonZeroU32; + use std::sync::LazyLock; // Default impl is for tests only, and really just so that tests can more // easily specify just the parts that they want. @@ -1201,6 +1204,49 @@ mod test { Uuid::parse_str(nexus_test_utils::RACK_UUID).unwrap() } + // Return a `BlueprintBuilder` configured from `system` and based on an + // empty parent blueprint. + // + // If used for RSS tests (most of the time!), the blueprint built by this + // builder will need to have its `parent_blueprint_id` manually set to + // `None` to erase the link to the empty parent. + fn blueprint_builder_with_empty_parent( + log: &Logger, + system: &SystemDescription, + test_name: &str, + ) -> BlueprintBuilder<'static> { + static EMPTY_BLUEPRINT: LazyLock = LazyLock::new(|| { + BlueprintBuilder::build_empty_with_sleds( + std::iter::empty(), + "EMPTY_BLUEPRINT static", + ) + }); + + let planning_input = system + .to_planning_input_builder() + .expect("created planning input builder") + .build(); + + let mut builder = BlueprintBuilder::new_based_on( + log, + &*EMPTY_BLUEPRINT, + &planning_input, + test_name, + PlannerRng::from_entropy(), + ) + .expect("created blueprint builder"); + + for (sled_id, sled_resources) in + planning_input.all_sled_resources(SledFilter::InService) + { + builder + .sled_add_disks(sled_id, &sled_resources) + .expect("added disks"); + } + + builder + } + #[tokio::test] async fn rack_set_initialized_empty() { let logctx = dev::test_setup_log("rack_set_initialized_empty"); @@ -1432,44 +1478,23 @@ mod test { let mut system = SystemDescription::new(); system - .set_external_ip_policy(external_ip_policy) + .set_external_ip_policy(external_ip_policy.clone()) .sled(SledBuilder::new().id(sled1.id())) .expect("failed to add sled1") .sled(SledBuilder::new().id(sled2.id())) .expect("failed to add sled2") .sled(SledBuilder::new().id(sled3.id())) .expect("failed to add sled3"); - let planning_input = system - .to_planning_input_builder() - .expect("created planning input builder") - .build(); - let empty_starting_blueprint = BlueprintBuilder::build_empty_with_sleds( - std::iter::empty(), - test_name, - ); + let mut builder = + blueprint_builder_with_empty_parent(&opctx.log, &system, test_name); + let mut external_networking_alloc = - ExternalNetworkingAllocator::from_blueprint( - &empty_starting_blueprint, - planning_input.external_ip_policy(), + ExternalNetworkingAllocator::from_current_zones( + &builder, + &external_ip_policy, ) .expect("constructed allocator"); - let mut builder = BlueprintBuilder::new_based_on( - &opctx.log, - &empty_starting_blueprint, - &planning_input, - test_name, - PlannerRng::from_entropy(), - ) - .expect("created blueprint builder"); - for (sled_id, sled_resources) in - planning_input.all_sled_resources(SledFilter::InService) - { - builder - .sled_add_disks(sled_id, &sled_resources) - .expect("added disks"); - } - let external_dns_networking = external_networking_alloc .for_new_external_dns() .expect("got IP for external DNS"); @@ -2256,74 +2281,37 @@ mod test { system .sled(SledBuilder::new().id(sled.id())) .expect("failed to add sled"); + let mut builder = + blueprint_builder_with_empty_parent(&opctx.log, &system, test_name); + // We didn't add anything to the `system` IP pool, but pick an IP + // anyway. This should fail below. let nexus_ip = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)); let nexus_pip = NEXUS_OPTE_IPV4_SUBNET .nth(NUM_INITIAL_RESERVED_IP_ADDRESSES + 1) .unwrap(); - let nexus_id = OmicronZoneUuid::new_v4(); let mut macs = MacAddr::iter_system(); - let mut blueprint_zones = BTreeMap::new(); - blueprint_zones.insert( - sled.id(), - [BlueprintZoneConfig { - disposition: BlueprintZoneDisposition::InService, - id: nexus_id, - filesystem_pool: random_zpool(), - zone_type: BlueprintZoneType::Nexus( - blueprint_zone_type::Nexus { - internal_address: "[::1]:80".parse().unwrap(), - lockstep_port: - omicron_common::address::NEXUS_LOCKSTEP_PORT, - external_ip: OmicronZoneExternalFloatingIp { - id: ExternalIpUuid::new_v4(), - ip: nexus_ip, - }, - external_tls: false, - external_dns_servers: vec![], - nic: NetworkInterface { - id: Uuid::new_v4(), - kind: NetworkInterfaceKind::Service { - id: nexus_id.into_untyped_uuid(), - }, - name: "nexus".parse().unwrap(), - ip: nexus_pip.into(), - mac: macs.next().unwrap(), - subnet: IpNet::from(*NEXUS_OPTE_IPV4_SUBNET), - vni: Vni::SERVICES_VNI, - primary: true, - slot: 0, - transit_ips: vec![], - }, - nexus_generation: *Generation::new(), - }, - ), - image_source: BlueprintZoneImageSource::InstallDataset, - }] - .into_iter() - .collect::>(), - ); - let blueprint_id = BlueprintUuid::new_v4(); - let blueprint = Blueprint { - id: blueprint_id, - sleds: make_sled_config_only_zones(blueprint_zones), - pending_mgs_updates: PendingMgsUpdates::new(), - cockroachdb_setting_preserve_downgrade: - CockroachDbPreserveDowngrade::DoNotModify, - parent_blueprint_id: None, - internal_dns_version: *Generation::new(), - external_dns_version: *Generation::new(), - target_release_minimum_generation: *Generation::new(), - nexus_generation: *Generation::new(), - cockroachdb_fingerprint: String::new(), - clickhouse_cluster_config: None, - oximeter_read_version: *Generation::new(), - oximeter_read_mode: OximeterReadMode::SingleNode, - time_created: now_db_precision(), - creator: "test suite".to_string(), - comment: "test blueprint".to_string(), - source: BlueprintSource::Test, - }; + builder + .sled_add_zone_nexus_with_config( + sled.id(), + false, + Vec::new(), + BlueprintZoneImageSource::InstallDataset, + ExternalNetworkingChoice { + external_ip: nexus_ip, + nic_ip: nexus_pip.into(), + nic_subnet: IpNet::from(*NEXUS_OPTE_IPV4_SUBNET), + nic_mac: macs.next().unwrap(), + }, + *Generation::new(), + ) + .expect("added Nexus"); + + let mut blueprint = builder.build(BlueprintSource::Test); + + // We're emulating RSS, which inserts the initial blueprint. Clear out + // the link back to the empty parent we started with. + blueprint.parent_blueprint_id = None; let result = datastore .rack_set_initialized( diff --git a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs index 5455805d804..c106bbfa0e8 100644 --- a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs +++ b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs @@ -606,7 +606,7 @@ impl<'a> BlueprintBuilder<'a> { pub fn new_based_on( log: &Logger, parent_blueprint: &'a Blueprint, - input: &'a PlanningInput, + input: &PlanningInput, creator: &str, mut rng: PlannerRng, ) -> anyhow::Result> { From d00b536eff1fc60862ea26f4bf19d8f7237d8884 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 12 Nov 2025 13:13:05 -0500 Subject: [PATCH 04/24] rack_set_initialized_with_many_nexus_services() --- nexus/db-queries/src/db/datastore/rack.rs | 140 ++++------------------ 1 file changed, 26 insertions(+), 114 deletions(-) diff --git a/nexus/db-queries/src/db/datastore/rack.rs b/nexus/db-queries/src/db/datastore/rack.rs index 81d07cd328e..53ef7fd1242 100644 --- a/nexus/db-queries/src/db/datastore/rack.rs +++ b/nexus/db-queries/src/db/datastore/rack.rs @@ -1740,100 +1740,34 @@ mod test { let mut system = SystemDescription::new(); system - .set_external_ip_policy(external_ip_policy) + .set_external_ip_policy(external_ip_policy.clone()) .sled(SledBuilder::new().id(sled.id())) .expect("failed to add sled"); + let mut builder = + blueprint_builder_with_empty_parent(&opctx.log, &system, test_name); - let nexus_id1 = OmicronZoneUuid::new_v4(); - let nexus_id2 = OmicronZoneUuid::new_v4(); - let nexus_pip1 = NEXUS_OPTE_IPV4_SUBNET - .nth(NUM_INITIAL_RESERVED_IP_ADDRESSES + 1) - .unwrap(); - let nexus_pip2 = NEXUS_OPTE_IPV4_SUBNET - .nth(NUM_INITIAL_RESERVED_IP_ADDRESSES + 2) - .unwrap(); - let mut macs = MacAddr::iter_system(); - - let mut blueprint_zones = BTreeMap::new(); - blueprint_zones.insert( - sled.id(), - [ - BlueprintZoneConfig { - disposition: BlueprintZoneDisposition::InService, - id: nexus_id1, - filesystem_pool: random_zpool(), - zone_type: BlueprintZoneType::Nexus( - blueprint_zone_type::Nexus { - internal_address: "[::1]:80".parse().unwrap(), - lockstep_port: - omicron_common::address::NEXUS_LOCKSTEP_PORT, - external_ip: OmicronZoneExternalFloatingIp { - id: ExternalIpUuid::new_v4(), - ip: nexus_ip_start.into(), - }, - external_tls: false, - external_dns_servers: vec![], - nic: NetworkInterface { - id: Uuid::new_v4(), - kind: NetworkInterfaceKind::Service { - id: nexus_id1.into_untyped_uuid(), - }, - name: "nexus1".parse().unwrap(), - ip: nexus_pip1.into(), - mac: macs.next().unwrap(), - subnet: IpNet::from(*NEXUS_OPTE_IPV4_SUBNET), - vni: Vni::SERVICES_VNI, - primary: true, - slot: 0, - transit_ips: vec![], - }, - nexus_generation: *Generation::new(), - }, - ), - image_source: BlueprintZoneImageSource::InstallDataset, - }, - BlueprintZoneConfig { - disposition: BlueprintZoneDisposition::InService, - id: nexus_id2, - filesystem_pool: random_zpool(), - zone_type: BlueprintZoneType::Nexus( - blueprint_zone_type::Nexus { - internal_address: "[::1]:80".parse().unwrap(), - lockstep_port: - omicron_common::address::NEXUS_LOCKSTEP_PORT, - external_ip: OmicronZoneExternalFloatingIp { - id: ExternalIpUuid::new_v4(), - ip: nexus_ip_end.into(), - }, - external_tls: false, - external_dns_servers: vec![], - nic: NetworkInterface { - id: Uuid::new_v4(), - kind: NetworkInterfaceKind::Service { - id: nexus_id2.into_untyped_uuid(), - }, - name: "nexus2".parse().unwrap(), - ip: nexus_pip2.into(), - mac: macs.next().unwrap(), - subnet: oxnet::IpNet::from( - *NEXUS_OPTE_IPV4_SUBNET, - ), - vni: Vni::SERVICES_VNI, - primary: true, - slot: 0, - transit_ips: vec![], - }, - nexus_generation: *Generation::new(), - }, - ), - image_source: BlueprintZoneImageSource::InstallDataset, - }, - ] - .into_iter() - .collect::>(), - ); - - let datasets = vec![]; + let mut external_networking_alloc = + ExternalNetworkingAllocator::from_current_zones( + &builder, + &external_ip_policy, + ) + .expect("constructed allocator"); + for _ in 0..2 { + builder + .sled_add_zone_nexus_with_config( + sled.id(), + false, + Vec::new(), + BlueprintZoneImageSource::InstallDataset, + external_networking_alloc + .for_new_nexus() + .expect("got Nexus IP"), + *Generation::new(), + ) + .expect("added Nexus"); + } + let mut blueprint = builder.build(BlueprintSource::Test); + blueprint.parent_blueprint_id = None; // treat this as the initial bp let internal_records = vec![ DnsRecord::Aaaa("fe80::1:2:3:4".parse().unwrap()), @@ -1857,34 +1791,12 @@ mod test { HashMap::from([("api.sys".to_string(), external_records.clone())]), ); - let blueprint_id = BlueprintUuid::new_v4(); - let blueprint = Blueprint { - id: blueprint_id, - sleds: make_sled_config_only_zones(blueprint_zones), - pending_mgs_updates: PendingMgsUpdates::new(), - cockroachdb_setting_preserve_downgrade: - CockroachDbPreserveDowngrade::DoNotModify, - parent_blueprint_id: None, - internal_dns_version: *Generation::new(), - external_dns_version: *Generation::new(), - target_release_minimum_generation: *Generation::new(), - nexus_generation: *Generation::new(), - cockroachdb_fingerprint: String::new(), - clickhouse_cluster_config: None, - oximeter_read_version: *Generation::new(), - oximeter_read_mode: OximeterReadMode::SingleNode, - time_created: now_db_precision(), - creator: "test suite".to_string(), - comment: "test blueprint".to_string(), - source: BlueprintSource::Test, - }; - let rack = datastore .rack_set_initialized( &opctx, RackInit { blueprint: blueprint.clone(), - datasets: datasets.clone(), + datasets: vec![], service_ip_pool_ranges: vec![service_ip_pool], internal_dns, external_dns, From 8302b01d5265b1cd77add60619036a405da742fe Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 12 Nov 2025 13:16:43 -0500 Subject: [PATCH 05/24] rack_set_initialized_with_ipv6_public_addresses() --- nexus/db-queries/src/db/datastore/rack.rs | 91 ++++++----------------- 1 file changed, 23 insertions(+), 68 deletions(-) diff --git a/nexus/db-queries/src/db/datastore/rack.rs b/nexus/db-queries/src/db/datastore/rack.rs index 53ef7fd1242..540ec7bdef9 100644 --- a/nexus/db-queries/src/db/datastore/rack.rs +++ b/nexus/db-queries/src/db/datastore/rack.rs @@ -1945,56 +1945,33 @@ mod test { let mut system = SystemDescription::new(); system - .set_external_ip_policy(external_ip_policy) + .set_external_ip_policy(external_ip_policy.clone()) .sled(SledBuilder::new().id(sled.id())) .expect("failed to add sled"); - let nexus_id = OmicronZoneUuid::new_v4(); - let nexus_pip = NEXUS_OPTE_IPV6_SUBNET - .nth(NUM_INITIAL_RESERVED_IP_ADDRESSES as u128 + 1) - .unwrap(); - let mut macs = MacAddr::iter_system(); + let mut builder = + blueprint_builder_with_empty_parent(&opctx.log, &system, test_name); - let mut blueprint_zones = BTreeMap::new(); - blueprint_zones.insert( - sled.id(), - [BlueprintZoneConfig { - disposition: BlueprintZoneDisposition::InService, - id: nexus_id, - filesystem_pool: random_zpool(), - zone_type: BlueprintZoneType::Nexus( - blueprint_zone_type::Nexus { - internal_address: "[::1]:80".parse().unwrap(), - lockstep_port: - omicron_common::address::NEXUS_LOCKSTEP_PORT, - external_ip: OmicronZoneExternalFloatingIp { - id: ExternalIpUuid::new_v4(), - ip: nexus_ip_start.into(), - }, - external_tls: false, - external_dns_servers: vec![], - nic: NetworkInterface { - id: Uuid::new_v4(), - kind: NetworkInterfaceKind::Service { - id: nexus_id.into_untyped_uuid(), - }, - name: "nexus1".parse().unwrap(), - ip: nexus_pip.into(), - mac: macs.next().unwrap(), - subnet: IpNet::from(*NEXUS_OPTE_IPV6_SUBNET), - vni: Vni::SERVICES_VNI, - primary: true, - slot: 0, - transit_ips: vec![], - }, - nexus_generation: *Generation::new(), - }, - ), - image_source: BlueprintZoneImageSource::InstallDataset, - }] - .into_iter() - .collect::>(), - ); + let mut external_networking_alloc = + ExternalNetworkingAllocator::from_current_zones( + &builder, + &external_ip_policy, + ) + .expect("constructed allocator"); + builder + .sled_add_zone_nexus_with_config( + sled.id(), + false, + Vec::new(), + BlueprintZoneImageSource::InstallDataset, + external_networking_alloc + .for_new_nexus() + .expect("got Nexus IP"), + *Generation::new(), + ) + .expect("added Nexus"); + let mut blueprint = builder.build(BlueprintSource::Test); + blueprint.parent_blueprint_id = None; // treat this as the initial bp let datasets = vec![]; @@ -2020,28 +1997,6 @@ mod test { HashMap::from([("api.sys".to_string(), external_records.clone())]), ); - let blueprint_id = BlueprintUuid::new_v4(); - let blueprint = Blueprint { - id: blueprint_id, - sleds: make_sled_config_only_zones(blueprint_zones), - pending_mgs_updates: PendingMgsUpdates::new(), - cockroachdb_setting_preserve_downgrade: - CockroachDbPreserveDowngrade::DoNotModify, - parent_blueprint_id: None, - internal_dns_version: *Generation::new(), - external_dns_version: *Generation::new(), - target_release_minimum_generation: *Generation::new(), - cockroachdb_fingerprint: String::new(), - clickhouse_cluster_config: None, - oximeter_read_version: *Generation::new(), - oximeter_read_mode: OximeterReadMode::SingleNode, - time_created: now_db_precision(), - creator: "test suite".to_string(), - comment: "test blueprint".to_string(), - source: BlueprintSource::Test, - nexus_generation: *Generation::new(), - }; - let rack = datastore .rack_set_initialized( &opctx, From ce1c5dc1aef5d16307418a563b79a6b4e5aeda8f Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 12 Nov 2025 14:07:17 -0500 Subject: [PATCH 06/24] rack_set_initialized_overlapping_ips_throws_error() --- nexus/db-queries/src/db/datastore/rack.rs | 197 ++++------------------ 1 file changed, 31 insertions(+), 166 deletions(-) diff --git a/nexus/db-queries/src/db/datastore/rack.rs b/nexus/db-queries/src/db/datastore/rack.rs index 540ec7bdef9..7deb85a0a0a 100644 --- a/nexus/db-queries/src/db/datastore/rack.rs +++ b/nexus/db-queries/src/db/datastore/rack.rs @@ -1062,11 +1062,9 @@ mod test { use crate::db::pub_test_utils::TestDatabase; use crate::db::pub_test_utils::helpers::SledUpdateBuilder; use async_bb8_diesel::AsyncSimpleConnection; - use iddqd::IdOrdMap; use internal_dns_types::names::DNS_ZONE; use nexus_config::NUM_INITIAL_RESERVED_IP_ADDRESSES; use nexus_db_model::{DnsGroup, Generation, InitialDnsGroup}; - use nexus_inventory::now_db_precision; use nexus_reconfigurator_planning::blueprint_builder::BlueprintBuilder; use nexus_reconfigurator_planning::blueprint_editor::ExternalNetworkingAllocator; use nexus_reconfigurator_planning::blueprint_editor::ExternalNetworkingChoice; @@ -1074,48 +1072,31 @@ mod test { use nexus_reconfigurator_planning::system::{ SledBuilder, SystemDescription, }; - use nexus_sled_agent_shared::inventory::OmicronZoneDataset; - use nexus_types::deployment::BlueprintHostPhase2DesiredSlots; - use nexus_types::deployment::BlueprintSledConfig; use nexus_types::deployment::BlueprintSource; use nexus_types::deployment::CockroachDbPreserveDowngrade; use nexus_types::deployment::ExternalIpPolicy; use nexus_types::deployment::PendingMgsUpdates; use nexus_types::deployment::SledFilter; - use nexus_types::deployment::{ - BlueprintZoneConfig, OmicronZoneExternalFloatingAddr, - OmicronZoneExternalFloatingIp, - }; use nexus_types::deployment::{ BlueprintZoneDisposition, BlueprintZoneImageSource, OximeterReadMode, }; use nexus_types::external_api::shared::SiloIdentityMode; - use nexus_types::external_api::views::SledState; use nexus_types::identity::Asset; use nexus_types::internal_api::params::DnsRecord; - use nexus_types::inventory::NetworkInterface; - use nexus_types::inventory::NetworkInterfaceKind; - use omicron_common::address::NEXUS_OPTE_IPV6_SUBNET; - use omicron_common::address::{ - DNS_OPTE_IPV4_SUBNET, NEXUS_OPTE_IPV4_SUBNET, - }; + use omicron_common::address::NEXUS_OPTE_IPV4_SUBNET; use omicron_common::api::external::http_pagination::PaginatedBy; use omicron_common::api::external::{ - IdentityMetadataCreateParams, MacAddr, Vni, + IdentityMetadataCreateParams, MacAddr, }; - use omicron_common::zpool_name::ZpoolName; use omicron_test_utils::dev; use omicron_uuid_kinds::BlueprintUuid; - use omicron_uuid_kinds::ExternalIpUuid; use omicron_uuid_kinds::GenericUuid; - use omicron_uuid_kinds::OmicronZoneUuid; use omicron_uuid_kinds::SledUuid; - use omicron_uuid_kinds::ZpoolUuid; use oxnet::IpNet; use slog::Logger; use std::collections::{BTreeMap, HashMap}; use std::net::Ipv6Addr; - use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::net::{IpAddr, Ipv4Addr}; use std::num::NonZeroU32; use std::sync::LazyLock; @@ -1229,7 +1210,7 @@ mod test { let mut builder = BlueprintBuilder::new_based_on( log, - &*EMPTY_BLUEPRINT, + &EMPTY_BLUEPRINT, &planning_input, test_name, PlannerRng::from_entropy(), @@ -1411,44 +1392,6 @@ mod test { fn_to_get_all!(ip_pool_range, IpPoolRange); fn_to_get_all!(crucible_dataset, CrucibleDataset); - fn random_zpool() -> ZpoolName { - ZpoolName::new_external(ZpoolUuid::new_v4()) - } - - fn random_dataset() -> OmicronZoneDataset { - OmicronZoneDataset { - pool_name: illumos_utils::zpool::ZpoolName::new_external( - ZpoolUuid::new_v4(), - ) - .to_string() - .parse() - .unwrap(), - } - } - - fn make_sled_config_only_zones( - blueprint_zones: BTreeMap>, - ) -> BTreeMap { - blueprint_zones - .into_iter() - .map(|(sled_id, zones)| { - ( - sled_id, - BlueprintSledConfig { - state: SledState::Active, - sled_agent_generation: Generation::new().next(), - disks: IdOrdMap::new(), - datasets: IdOrdMap::new(), - zones, - remove_mupdate_override: None, - host_phase_2: - BlueprintHostPhase2DesiredSlots::current_contents(), - }, - ) - }) - .collect() - } - #[tokio::test] async fn rack_set_initialized_with_services() { let test_name = "rack_set_initialized_with_services"; @@ -2215,116 +2158,38 @@ mod test { let mut system = SystemDescription::new(); system - .set_external_ip_policy(external_ip_policy) + .set_external_ip_policy(external_ip_policy.clone()) .sled(SledBuilder::new().id(sled.id())) .expect("failed to add sled"); - // Request two services which happen to be using the same IP address. - let external_dns_id = OmicronZoneUuid::new_v4(); - let external_dns_pip = DNS_OPTE_IPV4_SUBNET - .nth(NUM_INITIAL_RESERVED_IP_ADDRESSES + 1) - .unwrap(); - let nexus_id = OmicronZoneUuid::new_v4(); - let nexus_pip = NEXUS_OPTE_IPV4_SUBNET - .nth(NUM_INITIAL_RESERVED_IP_ADDRESSES + 1) - .unwrap(); - let mut macs = MacAddr::iter_system(); + let mut builder = + blueprint_builder_with_empty_parent(&opctx.log, &system, test_name); - let mut blueprint_zones = BTreeMap::new(); - let dataset = random_dataset(); - blueprint_zones.insert( - sled.id(), - [ - BlueprintZoneConfig { - disposition: BlueprintZoneDisposition::InService, - id: external_dns_id, - filesystem_pool: dataset.pool_name, - zone_type: BlueprintZoneType::ExternalDns( - blueprint_zone_type::ExternalDns { - dataset, - http_address: "[::1]:80".parse().unwrap(), - dns_address: OmicronZoneExternalFloatingAddr { - id: ExternalIpUuid::new_v4(), - addr: SocketAddr::new(ip, 53), - }, - nic: NetworkInterface { - id: Uuid::new_v4(), - kind: NetworkInterfaceKind::Service { - id: external_dns_id.into_untyped_uuid(), - }, - name: "external-dns".parse().unwrap(), - ip: external_dns_pip.into(), - mac: macs.next().unwrap(), - subnet: IpNet::from(*DNS_OPTE_IPV4_SUBNET), - vni: Vni::SERVICES_VNI, - primary: true, - slot: 0, - transit_ips: vec![], - }, - }, - ), - image_source: BlueprintZoneImageSource::InstallDataset, - }, - BlueprintZoneConfig { - disposition: BlueprintZoneDisposition::InService, - id: nexus_id, - filesystem_pool: random_zpool(), - zone_type: BlueprintZoneType::Nexus( - blueprint_zone_type::Nexus { - internal_address: "[::1]:80".parse().unwrap(), - lockstep_port: - omicron_common::address::NEXUS_LOCKSTEP_PORT, - external_ip: OmicronZoneExternalFloatingIp { - id: ExternalIpUuid::new_v4(), - ip, - }, - external_tls: false, - external_dns_servers: vec![], - nic: NetworkInterface { - id: Uuid::new_v4(), - kind: NetworkInterfaceKind::Service { - id: nexus_id.into_untyped_uuid(), - }, - name: "nexus".parse().unwrap(), - ip: nexus_pip.into(), - mac: macs.next().unwrap(), - subnet: IpNet::from(*NEXUS_OPTE_IPV4_SUBNET), - vni: Vni::SERVICES_VNI, - primary: true, - slot: 0, - transit_ips: vec![], - }, - nexus_generation: *Generation::new(), - }, - ), - image_source: BlueprintZoneImageSource::InstallDataset, - }, - ] - .into_iter() - .collect::>(), - ); + let mut external_networking_alloc = + ExternalNetworkingAllocator::from_current_zones( + &builder, + &external_ip_policy, + ) + .expect("constructed allocator"); - let blueprint_id = BlueprintUuid::new_v4(); - let blueprint = Blueprint { - id: blueprint_id, - sleds: make_sled_config_only_zones(blueprint_zones), - pending_mgs_updates: PendingMgsUpdates::new(), - cockroachdb_setting_preserve_downgrade: - CockroachDbPreserveDowngrade::DoNotModify, - parent_blueprint_id: None, - internal_dns_version: *Generation::new(), - external_dns_version: *Generation::new(), - target_release_minimum_generation: *Generation::new(), - nexus_generation: *Generation::new(), - cockroachdb_fingerprint: String::new(), - clickhouse_cluster_config: None, - oximeter_read_version: *Generation::new(), - oximeter_read_mode: OximeterReadMode::SingleNode, - time_created: now_db_precision(), - creator: "test suite".to_string(), - comment: "test blueprint".to_string(), - source: BlueprintSource::Test, - }; + // Add two Nexus zones, but reuse the same external IP for both. + let nexus_external_ip = + external_networking_alloc.for_new_nexus().expect("got Nexus IP"); + for _ in 0..2 { + builder + .sled_add_zone_nexus_with_config( + sled.id(), + false, + Vec::new(), + BlueprintZoneImageSource::InstallDataset, + nexus_external_ip, + *Generation::new(), + ) + .expect("added Nexus"); + } + + let mut blueprint = builder.build(BlueprintSource::Test); + blueprint.parent_blueprint_id = None; // treat this as the initial bp let result = datastore .rack_set_initialized( From fc4658d75e9657aa1ba1c107c4800dfa1d7f80b7 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 12 Nov 2025 14:11:52 -0500 Subject: [PATCH 07/24] revert now-unneccesary visibility change --- .../src/blueprint_editor/allocators/external_networking.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nexus/reconfigurator/planning/src/blueprint_editor/allocators/external_networking.rs b/nexus/reconfigurator/planning/src/blueprint_editor/allocators/external_networking.rs index 979cd1bd453..53703f6a7ab 100644 --- a/nexus/reconfigurator/planning/src/blueprint_editor/allocators/external_networking.rs +++ b/nexus/reconfigurator/planning/src/blueprint_editor/allocators/external_networking.rs @@ -73,7 +73,8 @@ impl ExternalNetworkingAllocator { /// Construct an `ExternalNetworkingAllocator` that hands out IPs based on /// `external_ip_policy`, treating any IPs used by in-service zones /// in `blueprint` as already-in-use. - pub fn from_blueprint( + #[cfg(test)] + pub(crate) fn from_blueprint( blueprint: &nexus_types::deployment::Blueprint, external_ip_policy: &ExternalIpPolicy, ) -> anyhow::Result { From 3373282973474a72ccbfa2cbcf45bb728955925c Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 12 Nov 2025 14:20:22 -0500 Subject: [PATCH 08/24] add BlueprintBuilder::build_empty() --- nexus/db-queries/src/db/datastore/deployment.rs | 15 +++------------ nexus/db-queries/src/db/datastore/rack.rs | 5 +---- nexus/reconfigurator/execution/src/dns.rs | 5 +---- .../planning/src/blueprint_builder/builder.rs | 6 ++++++ .../src/app/background/tasks/blueprint_planner.rs | 14 ++++++-------- 5 files changed, 17 insertions(+), 28 deletions(-) diff --git a/nexus/db-queries/src/db/datastore/deployment.rs b/nexus/db-queries/src/db/datastore/deployment.rs index 6bfb26db881..9a669cbde84 100644 --- a/nexus/db-queries/src/db/datastore/deployment.rs +++ b/nexus/db-queries/src/db/datastore/deployment.rs @@ -3311,10 +3311,7 @@ mod tests { let (opctx, datastore) = (db.opctx(), db.datastore()); // Create an empty blueprint from it - let blueprint1 = BlueprintBuilder::build_empty_with_sleds( - std::iter::empty(), - "test", - ); + let blueprint1 = BlueprintBuilder::build_empty("test"); let authz_blueprint = authz_blueprint_from_id(blueprint1.id); // Trying to read it from the database should fail with the relevant @@ -4039,10 +4036,7 @@ mod tests { // Create three blueprints: // * `blueprint1` has no parent // * `blueprint2` and `blueprint3` both have `blueprint1` as parent - let blueprint1 = BlueprintBuilder::build_empty_with_sleds( - std::iter::empty(), - "test1", - ); + let blueprint1 = BlueprintBuilder::build_empty("test1"); let blueprint2 = BlueprintBuilder::new_based_on( &logctx.log, &blueprint1, @@ -4190,10 +4184,7 @@ mod tests { let (opctx, datastore) = (db.opctx(), db.datastore()); // Create an initial blueprint and a child. - let blueprint1 = BlueprintBuilder::build_empty_with_sleds( - std::iter::empty(), - "test1", - ); + let blueprint1 = BlueprintBuilder::build_empty("test1"); let blueprint2 = BlueprintBuilder::new_based_on( &logctx.log, &blueprint1, diff --git a/nexus/db-queries/src/db/datastore/rack.rs b/nexus/db-queries/src/db/datastore/rack.rs index 7deb85a0a0a..274a26cb3b5 100644 --- a/nexus/db-queries/src/db/datastore/rack.rs +++ b/nexus/db-queries/src/db/datastore/rack.rs @@ -1197,10 +1197,7 @@ mod test { test_name: &str, ) -> BlueprintBuilder<'static> { static EMPTY_BLUEPRINT: LazyLock = LazyLock::new(|| { - BlueprintBuilder::build_empty_with_sleds( - std::iter::empty(), - "EMPTY_BLUEPRINT static", - ) + BlueprintBuilder::build_empty("EMPTY_BLUEPRINT static") }); let planning_input = system diff --git a/nexus/reconfigurator/execution/src/dns.rs b/nexus/reconfigurator/execution/src/dns.rs index e92a82cefe9..fb932f75792 100644 --- a/nexus/reconfigurator/execution/src/dns.rs +++ b/nexus/reconfigurator/execution/src/dns.rs @@ -650,10 +650,7 @@ mod test { /// test blueprint_internal_dns_config(): trivial case of an empty blueprint #[test] fn test_blueprint_internal_dns_empty() { - let blueprint = BlueprintBuilder::build_empty_with_sleds( - std::iter::empty(), - "test-suite", - ); + let blueprint = BlueprintBuilder::build_empty("test-suite"); let blueprint_dns = blueprint_internal_dns_config( &blueprint, &IdOrdMap::new(), diff --git a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs index c106bbfa0e8..824081e01ff 100644 --- a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs +++ b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs @@ -530,6 +530,12 @@ pub struct BlueprintBuilder<'a> { } impl<'a> BlueprintBuilder<'a> { + /// Directly construct an empty blueprint: no sleds; default values for all + /// other fields. + pub fn build_empty(creator: &str) -> Blueprint { + Self::build_empty_with_sleds(iter::empty(), creator) + } + /// Directly construct a `Blueprint` that contains an empty zone config for /// the given sleds. pub fn build_empty_with_sleds( diff --git a/nexus/src/app/background/tasks/blueprint_planner.rs b/nexus/src/app/background/tasks/blueprint_planner.rs index 4d9d87c0212..cd037aab251 100644 --- a/nexus/src/app/background/tasks/blueprint_planner.rs +++ b/nexus/src/app/background/tasks/blueprint_planner.rs @@ -638,10 +638,10 @@ mod test { // Create a large number of blueprints (49), which we'll use to test the // limit (see below). for i in 0..49 { - let blueprint = BlueprintBuilder::build_empty_with_sleds( - std::iter::empty(), - &format!("test_blueprint_planner_limit blueprint {}", i), - ); + let blueprint = BlueprintBuilder::build_empty(&format!( + "test_blueprint_planner_limit blueprint {}", + i + )); datastore .blueprint_insert(&opctx, &blueprint) .await @@ -691,8 +691,7 @@ mod test { // Insert one more blueprint, pushing the number of blueprints to the // limit (50). - let blueprint = BlueprintBuilder::build_empty_with_sleds( - std::iter::empty(), + let blueprint = BlueprintBuilder::build_empty( "test_blueprint_planner_limit 50th blueprint", ); datastore.blueprint_insert(&opctx, &blueprint).await.unwrap_or_else( @@ -715,8 +714,7 @@ mod test { ); // But manual planning should continue to work. - let blueprint = BlueprintBuilder::build_empty_with_sleds( - std::iter::empty(), + let blueprint = BlueprintBuilder::build_empty( "test_blueprint_planner_limit 51st blueprint", ); datastore.blueprint_insert(&opctx, &blueprint).await.unwrap_or_else( From efd74fad8adac2be1936a27558f31aa92b7d8f30 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 12 Nov 2025 14:37:29 -0500 Subject: [PATCH 09/24] example system: start with truly empty blueprint --- .../output/cmds-blueprint-history-stdout | 28 +++++++++---------- .../tests/output/cmds-example-stdout | 16 +++++------ .../planning/src/blueprint_builder/builder.rs | 6 ++++ nexus/reconfigurator/planning/src/example.rs | 5 ++-- 4 files changed, 30 insertions(+), 25 deletions(-) diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-blueprint-history-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-blueprint-history-stdout index 2488810e455..72ec7dd7aab 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-blueprint-history-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-blueprint-history-stdout @@ -26,7 +26,7 @@ blueprint df06bb57-ad42-4431-9206-abff322896c7 created from blueprint af934083-5 > # which does not include any of those blueprints. > blueprint-history TIME BLUEPRINT - 184f10b3-61cb-41ef-9b93-3489b2bac559 starting blueprint with 3 empty sleds + 184f10b3-61cb-41ef-9b93-3489b2bac559 starting blueprint with 0 empty sleds dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 @@ -34,7 +34,7 @@ TIME BLUEPRINT > # You can give it a specific blueprint. > blueprint-history 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 TIME BLUEPRINT - 184f10b3-61cb-41ef-9b93-3489b2bac559 starting blueprint with 3 empty sleds + 184f10b3-61cb-41ef-9b93-3489b2bac559 starting blueprint with 0 empty sleds dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 @@ -43,7 +43,7 @@ TIME BLUEPRINT > # Running it from the latest blueprint should report all of them. > blueprint-history latest TIME BLUEPRINT - 184f10b3-61cb-41ef-9b93-3489b2bac559 starting blueprint with 3 empty sleds + 184f10b3-61cb-41ef-9b93-3489b2bac559 starting blueprint with 0 empty sleds dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 58d5e830-0884-47d8-a7cd-b2b3751adeb4 @@ -64,21 +64,21 @@ TIME BLUEPRINT > # Show diffs, too. > blueprint-history --diff latest TIME BLUEPRINT - 184f10b3-61cb-41ef-9b93-3489b2bac559 starting blueprint with 3 empty sleds + 184f10b3-61cb-41ef-9b93-3489b2bac559 starting blueprint with 0 empty sleds dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 from: blueprint 184f10b3-61cb-41ef-9b93-3489b2bac559 to: blueprint dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 - MODIFIED SLEDS: + ADDED SLEDS: - sled 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c (active, config generation 1 -> 2): + sled 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c (active, config generation 2): host phase 2 contents: ------------------------ slot boot image source ------------------------ - A current contents - B current contents ++ A current contents ++ B current contents physical disks: @@ -184,14 +184,14 @@ to: blueprint dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 + nexus 466a9f29-62bf-4e63-924a-b9efdb86afec install dataset in service fd00:1122:3344:102::22 - sled 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (active, config generation 1 -> 2): + sled 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (active, config generation 2): host phase 2 contents: ------------------------ slot boot image source ------------------------ - A current contents - B current contents ++ A current contents ++ B current contents physical disks: @@ -294,14 +294,14 @@ to: blueprint dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 + nexus 0c71b3b2-6ceb-4e8f-b020-b08675e83038 install dataset in service fd00:1122:3344:101::22 - sled d81c6a84-79b8-4958-ae41-ea46c9b19763 (active, config generation 1 -> 2): + sled d81c6a84-79b8-4958-ae41-ea46c9b19763 (active, config generation 2): host phase 2 contents: ------------------------ slot boot image source ------------------------ - A current contents - B current contents ++ A current contents ++ B current contents physical disks: diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-example-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-example-stdout index 8477b992a1e..a3028f9b689 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-example-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-example-stdout @@ -694,16 +694,16 @@ external DNS: from: blueprint 02697f74-b14a-4418-90f0-c28b2a3a6aa9 to: blueprint 86db3308-f817-4626-8838-4085949a6a41 - MODIFIED SLEDS: + ADDED SLEDS: - sled 89d02b1b-478c-401a-8e28-7a26f74fa41b (active, config generation 1 -> 2): + sled 89d02b1b-478c-401a-8e28-7a26f74fa41b (active, config generation 2): host phase 2 contents: ------------------------ slot boot image source ------------------------ - A current contents - B current contents ++ A current contents ++ B current contents physical disks: @@ -914,16 +914,16 @@ external DNS: from: blueprint 86db3308-f817-4626-8838-4085949a6a41 to: blueprint 02697f74-b14a-4418-90f0-c28b2a3a6aa9 - MODIFIED SLEDS: + REMOVED SLEDS: - sled 89d02b1b-478c-401a-8e28-7a26f74fa41b (active, config generation 2 -> 1): + sled 89d02b1b-478c-401a-8e28-7a26f74fa41b (was active, config generation 2): host phase 2 contents: ------------------------ slot boot image source ------------------------ - A current contents - B current contents +- A current contents +- B current contents physical disks: diff --git a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs index 824081e01ff..8f189f66673 100644 --- a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs +++ b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs @@ -536,6 +536,12 @@ impl<'a> BlueprintBuilder<'a> { Self::build_empty_with_sleds(iter::empty(), creator) } + /// A version of [`Self::build_empty`] that allows the + /// blueprint ID to be generated from a deterministic RNG. + pub fn build_empty_seeded(creator: &str, rng: PlannerRng) -> Blueprint { + Self::build_empty_with_sleds_seeded(iter::empty(), creator, rng) + } + /// Directly construct a `Blueprint` that contains an empty zone config for /// the given sleds. pub fn build_empty_with_sleds( diff --git a/nexus/reconfigurator/planning/src/example.rs b/nexus/reconfigurator/planning/src/example.rs index c2291538cb8..4f1b1a10c67 100644 --- a/nexus/reconfigurator/planning/src/example.rs +++ b/nexus/reconfigurator/planning/src/example.rs @@ -535,9 +535,8 @@ impl ExampleSystemBuilder { let base_input = input_builder.clone().build(); - // Start with an empty blueprint containing only our sleds, no zones. - let initial_blueprint = BlueprintBuilder::build_empty_with_sleds_seeded( - base_input.all_sled_ids(SledFilter::Commissioned), + // Start with an empty blueprint. + let initial_blueprint = BlueprintBuilder::build_empty_seeded( "test suite", rng.blueprint1_rng, ); From 4b9eeaf08e0b16e30f409361f67034a58bfdb17a Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 12 Nov 2025 14:55:47 -0500 Subject: [PATCH 10/24] use build_empty() where we can --- nexus/db-queries/src/db/datastore/vpc.rs | 5 +---- .../src/app/background/tasks/crdb_node_id_collector.rs | 10 ++-------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/nexus/db-queries/src/db/datastore/vpc.rs b/nexus/db-queries/src/db/datastore/vpc.rs index 8fb61894efa..682da75e243 100644 --- a/nexus/db-queries/src/db/datastore/vpc.rs +++ b/nexus/db-queries/src/db/datastore/vpc.rs @@ -3312,10 +3312,7 @@ mod tests { }; // Create an initial, empty blueprint, and make it the target. - let bp0 = BlueprintBuilder::build_empty_with_sleds( - sled_ids.iter().copied(), - "test", - ); + let bp0 = BlueprintBuilder::build_empty("test"); bp_insert_and_make_target(&opctx, &datastore, &bp0).await; // Our blueprint doesn't describe any services, so we shouldn't find any diff --git a/nexus/src/app/background/tasks/crdb_node_id_collector.rs b/nexus/src/app/background/tasks/crdb_node_id_collector.rs index e7923db4be8..51b655b818a 100644 --- a/nexus/src/app/background/tasks/crdb_node_id_collector.rs +++ b/nexus/src/app/background/tasks/crdb_node_id_collector.rs @@ -401,10 +401,7 @@ mod tests { let db = TestDatabase::new_with_datastore(&logctx.log).await; let (opctx, datastore) = (db.opctx(), db.datastore()); - let blueprint = BlueprintBuilder::build_empty_with_sleds( - iter::once(SledUuid::new_v4()), - "test", - ); + let blueprint = BlueprintBuilder::build_empty("test"); let blueprint_target = BlueprintTarget { target_id: blueprint.id, enabled: true, @@ -464,10 +461,7 @@ mod tests { let db = TestDatabase::new_with_datastore(&logctx.log).await; let (opctx, datastore) = (db.opctx(), db.datastore()); - let blueprint = BlueprintBuilder::build_empty_with_sleds( - iter::once(SledUuid::new_v4()), - "test", - ); + let blueprint = BlueprintBuilder::build_empty("test"); let blueprint_target = BlueprintTarget { target_id: blueprint.id, enabled: true, From ff1b86a116f05483d64cc6fc2bab22c5eee094b9 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 12 Nov 2025 15:56:16 -0500 Subject: [PATCH 11/24] use example system in test_ensure_external_networking_works_with_good_target() --- .../db-queries/src/db/datastore/deployment.rs | 142 +++++------------- nexus/types/src/deployment/planning_input.rs | 13 ++ 2 files changed, 47 insertions(+), 108 deletions(-) diff --git a/nexus/db-queries/src/db/datastore/deployment.rs b/nexus/db-queries/src/db/datastore/deployment.rs index 9a669cbde84..ffb44a69c7a 100644 --- a/nexus/db-queries/src/db/datastore/deployment.rs +++ b/nexus/db-queries/src/db/datastore/deployment.rs @@ -3146,12 +3146,9 @@ mod tests { use nexus_types::deployment::BlueprintHostPhase2DesiredContents; use nexus_types::deployment::BlueprintHostPhase2DesiredSlots; use nexus_types::deployment::BlueprintPhysicalDiskDisposition; - use nexus_types::deployment::BlueprintZoneConfig; use nexus_types::deployment::BlueprintZoneDisposition; use nexus_types::deployment::BlueprintZoneImageSource; - use nexus_types::deployment::BlueprintZoneType; use nexus_types::deployment::ExpectedActiveRotSlot; - use nexus_types::deployment::OmicronZoneExternalFloatingIp; use nexus_types::deployment::PendingMgsUpdate; use nexus_types::deployment::PlanningInput; use nexus_types::deployment::PlanningInputBuilder; @@ -3159,7 +3156,6 @@ mod tests { use nexus_types::deployment::SledDisk; use nexus_types::deployment::SledFilter; use nexus_types::deployment::SledResources; - use nexus_types::deployment::blueprint_zone_type; use nexus_types::external_api::views::PhysicalDiskPolicy; use nexus_types::external_api::views::PhysicalDiskState; use nexus_types::external_api::views::SledPolicy; @@ -3168,36 +3164,24 @@ mod tests { use nexus_types::inventory::Collection; use omicron_common::address::IpRange; use omicron_common::address::Ipv6Subnet; - use omicron_common::api::external::MacAddr; - use omicron_common::api::external::Name; use omicron_common::api::external::TufArtifactMeta; use omicron_common::api::external::TufRepoDescription; use omicron_common::api::external::TufRepoMeta; - use omicron_common::api::external::Vni; - use omicron_common::api::internal::shared::NetworkInterface; - use omicron_common::api::internal::shared::NetworkInterfaceKind; use omicron_common::disk::DiskIdentity; use omicron_common::disk::M2Slot; use omicron_common::update::ArtifactId; - use omicron_common::zpool_name::ZpoolName; use omicron_test_utils::dev; use omicron_test_utils::dev::poll::CondCheckError; use omicron_test_utils::dev::poll::wait_for_condition; - use omicron_uuid_kinds::ExternalIpUuid; use omicron_uuid_kinds::OmicronZoneUuid; use omicron_uuid_kinds::PhysicalDiskUuid; use omicron_uuid_kinds::SledUuid; use omicron_uuid_kinds::ZpoolUuid; - use oxnet::IpNet; use pretty_assertions::assert_eq; use rand::Rng; use std::collections::BTreeSet; use std::mem; - use std::net::IpAddr; - use std::net::Ipv4Addr; use std::net::Ipv6Addr; - use std::net::SocketAddrV6; - use std::str::FromStr; use std::sync::Arc; use std::sync::LazyLock; use std::sync::atomic::AtomicBool; @@ -4283,104 +4267,46 @@ mod tests { logctx.cleanup_successful(); } - async fn create_blueprint_with_external_ip( - datastore: &DataStore, - opctx: &OpContext, - ) -> Blueprint { - // Create an initial blueprint and a child. - let sled_id = SledUuid::new_v4(); - let mut blueprint = BlueprintBuilder::build_empty_with_sleds( - [sled_id].into_iter(), - "test1", - ); - - // To observe realistic database behavior, we need the invocation of - // "blueprint_ensure_external_networking_resources" to actually write something - // back to the database. - // - // While this is *mostly* made-up blueprint contents, the part that matters - // is that it's provisioning a zone (Nexus) which does have resources - // to be allocated. - let ip_range = IpRange::try_from(( - Ipv4Addr::new(10, 0, 0, 1), - Ipv4Addr::new(10, 0, 0, 10), - )) - .unwrap(); - let (service_authz_ip_pool, service_ip_pool) = datastore - .ip_pools_service_lookup(&opctx, IpVersion::V4) - .await - .expect("lookup service ip pool"); - datastore - .ip_pool_add_range( - &opctx, - &service_authz_ip_pool, - &service_ip_pool, - &ip_range, - ) - .await - .expect("add range to service ip pool"); - let zone_id = OmicronZoneUuid::new_v4(); - blueprint - .sleds - .get_mut(&sled_id) - .unwrap() - .zones - .insert_unique(BlueprintZoneConfig { - disposition: BlueprintZoneDisposition::InService, - id: zone_id, - filesystem_pool: ZpoolName::new_external(ZpoolUuid::new_v4()), - zone_type: BlueprintZoneType::Nexus( - blueprint_zone_type::Nexus { - internal_address: SocketAddrV6::new( - Ipv6Addr::LOCALHOST, - 0, - 0, - 0, - ), - lockstep_port: 0, - external_ip: OmicronZoneExternalFloatingIp { - id: ExternalIpUuid::new_v4(), - ip: "10.0.0.1".parse().unwrap(), - }, - nic: NetworkInterface { - id: Uuid::new_v4(), - kind: NetworkInterfaceKind::Service { - id: *zone_id.as_untyped_uuid(), - }, - name: Name::from_str("mynic").unwrap(), - ip: "172.30.2.6".parse().unwrap(), - mac: MacAddr::random_system(), - subnet: IpNet::host_net(IpAddr::V6( - Ipv6Addr::LOCALHOST, - )), - vni: Vni::random(), - primary: true, - slot: 1, - transit_ips: vec![], - }, - external_tls: false, - external_dns_servers: vec![], - nexus_generation: Generation::new(), - }, - ), - image_source: BlueprintZoneImageSource::InstallDataset, - }) - .expect("freshly generated zone IDs are unique"); - - blueprint - } - #[tokio::test] async fn test_ensure_external_networking_works_with_good_target() { + const TEST_NAME: &str = + "test_ensure_external_networking_works_with_good_target"; // Setup - let logctx = dev::test_setup_log( - "test_ensure_external_networking_works_with_good_target", - ); + let logctx = dev::test_setup_log(TEST_NAME); let db = TestDatabase::new_with_datastore(&logctx.log).await; let (opctx, datastore) = (db.opctx(), db.datastore()); - let blueprint = - create_blueprint_with_external_ip(&datastore, &opctx).await; + let (example, mut blueprint) = + ExampleSystemBuilder::new(&opctx.log, TEST_NAME).build(); + + // Insert the IP pool ranges used by our example system. + for pool_range in + example.system.external_ip_policy().clone().into_raw_ranges() + { + // This looks up the pool again for each range; we only need at most + // two (one V4, one V6), but our example system doesn't have many + // ranges so this should be fine. + let (service_authz_ip_pool, service_ip_pool) = datastore + .ip_pools_service_lookup(&opctx, pool_range.version().into()) + .await + .expect("lookup service ip pool"); + datastore + .ip_pool_add_range( + &opctx, + &service_authz_ip_pool, + &service_ip_pool, + &pool_range, + ) + .await + .expect("add range to service IP pool"); + } + + // `ExampleSystemBuilder` returns a blueprint that has an empty parent. + // To make `blueprint` the target, we have to either insert that parent + // and make it the target first, or modify `blueprint` to make it look + // like it's the original. The latter is shorter. + blueprint.parent_blueprint_id = None; + datastore.blueprint_insert(&opctx, &blueprint).await.unwrap(); let bp_target = BlueprintTarget { diff --git a/nexus/types/src/deployment/planning_input.rs b/nexus/types/src/deployment/planning_input.rs index b9a7ce27777..89cd8800248 100644 --- a/nexus/types/src/deployment/planning_input.rs +++ b/nexus/types/src/deployment/planning_input.rs @@ -1189,6 +1189,19 @@ impl ExternalIpPolicy { pub fn external_dns_ips(&self) -> &BTreeSet { &self.external_dns_ips } + + /// Consume this `ExternalIpPolicy`, returning an iterator of all of the + /// ranges contained in it. + /// + /// This destroys all meaningful information (e.g., v4 vs v6 pool; which IPs + /// are reserved for external DNS) and should only be used by tests. + pub fn into_raw_ranges(self) -> impl Iterator { + let v4_ranges = + self.service_pool_ipv4_ranges.into_iter().map(IpRange::from); + let v6_ranges = + self.service_pool_ipv6_ranges.into_iter().map(IpRange::from); + v4_ranges.chain(v6_ranges) + } } #[derive(Debug, Clone, Default)] From 8d968c524a805fb88244553537b7e682d8892705 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 12 Nov 2025 16:31:14 -0500 Subject: [PATCH 12/24] test_external_dns_external_ips_specified_by_rack_setup() --- .../deployment/external_networking.rs | 223 +++++++++--------- 1 file changed, 117 insertions(+), 106 deletions(-) diff --git a/nexus/db-queries/src/db/datastore/deployment/external_networking.rs b/nexus/db-queries/src/db/datastore/deployment/external_networking.rs index 3ae7e4b478d..0723873433c 100644 --- a/nexus/db-queries/src/db/datastore/deployment/external_networking.rs +++ b/nexus/db-queries/src/db/datastore/deployment/external_networking.rs @@ -513,7 +513,11 @@ mod tests { use nexus_config::NUM_INITIAL_RESERVED_IP_ADDRESSES; use nexus_db_model::SqlU16; use nexus_reconfigurator_planning::blueprint_builder::BlueprintBuilder; + use nexus_reconfigurator_planning::blueprint_editor::ExternalNetworkingAllocator; + use nexus_reconfigurator_planning::example::ExampleSystemBuilder; + use nexus_reconfigurator_planning::planner::PlannerRng; use nexus_sled_agent_shared::inventory::OmicronZoneDataset; + use nexus_types::deployment::BlueprintSource; use nexus_types::deployment::BlueprintTarget; use nexus_types::deployment::BlueprintZoneConfig; use nexus_types::deployment::BlueprintZoneImageSource; @@ -527,6 +531,7 @@ mod tests { use omicron_common::address::DNS_OPTE_IPV4_SUBNET; use omicron_common::address::IpRange; use omicron_common::address::IpRangeIter; + use omicron_common::address::Ipv4Range; use omicron_common::address::NEXUS_OPTE_IPV4_SUBNET; use omicron_common::address::NTP_OPTE_IPV4_SUBNET; use omicron_common::address::NUM_SOURCE_NAT_PORTS; @@ -535,10 +540,7 @@ mod tests { use omicron_common::api::external::Vni; use omicron_common::zpool_name::ZpoolName; use omicron_test_utils::dev; - use omicron_uuid_kinds::BlueprintUuid; use omicron_uuid_kinds::ExternalIpUuid; - use omicron_uuid_kinds::ExternalZpoolUuid; - use omicron_uuid_kinds::SledUuid; use omicron_uuid_kinds::ZpoolUuid; use oxnet::IpNet; use std::net::IpAddr; @@ -1348,149 +1350,158 @@ mod tests { const TEST_NAME: &str = "test_external_dns_external_ips_specified_by_rack_setup"; - // Helper closures to reduce boilerplate below. + // Helper closure to reduce boilerplate below. let make_bp_target = |blueprint_id| BlueprintTarget { target_id: blueprint_id, enabled: false, time_made_target: Utc::now(), }; - let mut opte_ip_iter = DNS_OPTE_IPV4_SUBNET.addr_iter(); - let mut mac_iter = MacAddr::iter_system(); - let mut make_external_dns_zone = |ip, disposition| { - let zone_id = OmicronZoneUuid::new_v4(); - let pool = ZpoolName::External(ExternalZpoolUuid::new_v4()); - BlueprintZoneConfig { - disposition, - id: zone_id, - filesystem_pool: pool, - zone_type: BlueprintZoneType::ExternalDns( - blueprint_zone_type::ExternalDns { - dataset: OmicronZoneDataset { pool_name: pool }, - http_address: "[::1]:0".parse().unwrap(), - dns_address: OmicronZoneExternalFloatingAddr { - id: ExternalIpUuid::new_v4(), - addr: SocketAddr::new(ip, 0), - }, - nic: NetworkInterface { - id: Uuid::new_v4(), - kind: NetworkInterfaceKind::Service { - id: zone_id.into_untyped_uuid(), - }, - name: "test-external-dns".parse().unwrap(), - ip: opte_ip_iter.next().unwrap().into(), - mac: mac_iter.next().unwrap(), - subnet: IpNet::from(*DNS_OPTE_IPV4_SUBNET), - vni: Vni::SERVICES_VNI, - primary: true, - slot: 0, - transit_ips: vec![], - }, - }, - ), - image_source: BlueprintZoneImageSource::InstallDataset, - } - }; // Set up. let logctx = dev::test_setup_log(TEST_NAME); let db = TestDatabase::new_with_datastore(&logctx.log).await; let (opctx, datastore) = (db.opctx(), db.datastore()); - // Create a blueprint with one sled and no zones. Insert it and make it - // the target. - let sled_id = SledUuid::new_v4(); - let bp0 = BlueprintBuilder::build_empty_with_sleds( - std::iter::once(sled_id), - TEST_NAME, - ); - datastore.blueprint_insert(opctx, &bp0).await.expect("inserted bp0"); - datastore - .blueprint_target_set_current(opctx, make_bp_target(bp0.id)) - .await - .expect("made bp0 the target"); + // Create a blueprint with no external DNS zones. + let (example_system, bp1) = + ExampleSystemBuilder::new(&opctx.log, TEST_NAME) + .external_dns_count(0) + .expect("external DNS count can be 0") + .build(); + + // Insert the example system's initial empty blueprint and the one we + // want to test. Advance the target to point to `blueprint`. + let bp0 = &example_system.initial_blueprint; + for bp in [bp0, &bp1] { + datastore.blueprint_insert(opctx, bp).await.expect("inserted bp"); + datastore + .blueprint_target_set_current(opctx, make_bp_target(bp.id)) + .await + .expect("made bp the target"); + } // No external DNS zones => no external DNS IPs. + assert!( + example_system + .input + .external_ip_policy() + .external_dns_ips() + .is_empty() + ); let external_dns_ips = datastore .external_dns_external_ips_specified_by_rack_setup(opctx) .await .expect("got external DNS IPs"); assert_eq!(external_dns_ips, BTreeSet::new()); - // Create a blueprint with three in-service external DNS zones. We - // should get their IPs back. - let expected_ips = ["192.168.1.1", "192.168.1.2", "192.168.1.3"] - .into_iter() - .map(|ip| ip.parse::().unwrap()) - .collect::>(); - let mut bp1 = bp0.clone(); - bp1.id = BlueprintUuid::new_v4(); - bp1.parent_blueprint_id = Some(bp0.id); - for &ip in &expected_ips { - bp1.sleds - .get_mut(&sled_id) - .unwrap() - .zones - .insert_unique(make_external_dns_zone( - ip, - BlueprintZoneDisposition::InService, - )) - .expect("freshly generated zone IDs are unique"); + // Extend the external IP policy to allow for external DNS. + let all_external_dns_ips = IpRange::V4(Ipv4Range { + first: "192.168.1.1".parse().unwrap(), + last: "192.168.1.5".parse().unwrap(), + }); + let input = { + let mut policy_builder = example_system + .input + .external_ip_policy() + .clone() + .into_builder(); + policy_builder + .push_service_pool_range(all_external_dns_ips) + .expect("valid range"); + for ip in all_external_dns_ips.iter() { + policy_builder.add_external_dns_ip(ip).expect("valid IP"); + } + + let mut input_builder = example_system.input.into_builder(); + input_builder.policy_mut().external_ips = policy_builder.build(); + input_builder.build() + }; + + // Add an in-service external DNS zone for each IP. + let mut builder = BlueprintBuilder::new_based_on( + &opctx.log, + &bp1, + &input, + TEST_NAME, + PlannerRng::from_entropy(), + ) + .expect("created builder"); + + let mut external_networking_alloc = + ExternalNetworkingAllocator::from_current_zones( + &builder, + input.external_ip_policy(), + ) + .expect("created allocator"); + + let sled_id = bp1.sleds().next().expect("at least 1 sled exists"); + for _ in all_external_dns_ips.iter() { + builder + .sled_add_zone_external_dns( + sled_id, + BlueprintZoneImageSource::InstallDataset, + external_networking_alloc + .for_new_external_dns() + .expect("got IP for external DNS"), + ) + .expect("added external DNS"); } - // Insert bp1 and make it the target. Confirm we get back the expected + // Insert bp2 and make it the target. Confirm we get back the expected // external DNS IPs. - datastore.blueprint_insert(opctx, &bp1).await.expect("inserted bp1"); + let bp2 = builder.build(BlueprintSource::Test); + let expected_ips = all_external_dns_ips.iter().collect::>(); + datastore.blueprint_insert(opctx, &bp2).await.expect("inserted bp2"); datastore - .blueprint_target_set_current(opctx, make_bp_target(bp1.id)) + .blueprint_target_set_current(opctx, make_bp_target(bp2.id)) .await - .expect("made bp1 the target"); + .expect("made bp2 the target"); let external_dns_ips = datastore .external_dns_external_ips_specified_by_rack_setup(opctx) .await .expect("got external DNS IPs"); assert_eq!(external_dns_ips, expected_ips); - // Create a third blueprint with multiple expunged external DNS zones - // covering a couple additional IPs. Those should also be returned. - let extra_ips = ["192.168.1.4", "192.168.1.5"] - .into_iter() - .map(|ip| ip.parse::().unwrap()) + // Create a new blueprint that expunges two of those zones. + let mut builder = BlueprintBuilder::new_based_on( + &opctx.log, + &bp2, + &input, + TEST_NAME, + PlannerRng::from_entropy(), + ) + .expect("created builder"); + + let to_expunge = builder + .current_zones(BlueprintZoneDisposition::is_in_service) + .filter_map(|(sled_id, zone)| { + if zone.zone_type.is_external_dns() { + Some((sled_id, zone.id)) + } else { + None + } + }) + .take(2) .collect::>(); - assert_eq!(expected_ips.intersection(&extra_ips).count(), 0); - - let mut bp2 = bp1.clone(); - bp2.id = BlueprintUuid::new_v4(); - bp2.parent_blueprint_id = Some(bp1.id); - for &ip in &extra_ips { - for i in 0..4 { - bp2.sleds - .get_mut(&sled_id) - .unwrap() - .zones - .insert_unique(make_external_dns_zone( - ip, - BlueprintZoneDisposition::Expunged { - as_of_generation: Generation::new(), - ready_for_cleanup: i % 2 == 0, - }, - )) - .expect("freshly generated zone IDs are unique"); - } + assert_eq!(to_expunge.len(), 2); + + for (sled_id, zone_id) in to_expunge { + builder.sled_expunge_zone(sled_id, zone_id).expect("expunged zone"); } - // Insert bp1 and make it the target. Confirm we get back the expected + // Insert bp3 and make it the target. Confirm we still get back all five // external DNS IPs. - datastore.blueprint_insert(opctx, &bp2).await.expect("inserted bp2"); + let bp3 = builder.build(BlueprintSource::Test); + let expected_ips = all_external_dns_ips.iter().collect::>(); + datastore.blueprint_insert(opctx, &bp3).await.expect("inserted bp3"); datastore - .blueprint_target_set_current(opctx, make_bp_target(bp2.id)) + .blueprint_target_set_current(opctx, make_bp_target(bp3.id)) .await - .expect("made bp2 the target"); + .expect("made bp3 the target"); let external_dns_ips = datastore .external_dns_external_ips_specified_by_rack_setup(opctx) .await .expect("got external DNS IPs"); - let expected_ips = - expected_ips.union(&extra_ips).copied().collect::>(); assert_eq!(external_dns_ips, expected_ips); // Clean up. From 3efe2c0b071d3786a2534572cd3ac845464e732c Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 12 Nov 2025 16:48:29 -0500 Subject: [PATCH 13/24] test_default_cockroach_admin_addrs_from_blueprint() --- .../tasks/crdb_node_id_collector.rs | 180 ++++++++---------- 1 file changed, 76 insertions(+), 104 deletions(-) diff --git a/nexus/src/app/background/tasks/crdb_node_id_collector.rs b/nexus/src/app/background/tasks/crdb_node_id_collector.rs index 51b655b818a..f9563a17299 100644 --- a/nexus/src/app/background/tasks/crdb_node_id_collector.rs +++ b/nexus/src/app/background/tasks/crdb_node_id_collector.rs @@ -241,17 +241,15 @@ mod tests { use httptest::responders::status_code; use nexus_db_queries::db::pub_test_utils::TestDatabase; use nexus_reconfigurator_planning::blueprint_builder::BlueprintBuilder; - use nexus_sled_agent_shared::inventory::OmicronZoneDataset; - use nexus_types::deployment::BlueprintZoneConfig; + use nexus_reconfigurator_planning::example::ExampleSystemBuilder; + use nexus_reconfigurator_planning::planner::PlannerRng; + use nexus_types::deployment::BlueprintSource; use nexus_types::deployment::BlueprintZoneDisposition; use nexus_types::deployment::BlueprintZoneImageSource; use omicron_common::api::external::Generation; - use omicron_common::zpool_name::ZpoolName; use omicron_test_utils::dev; - use omicron_uuid_kinds::SledUuid; - use omicron_uuid_kinds::ZpoolUuid; use std::collections::BTreeMap; - use std::iter; + use std::collections::BTreeSet; use std::net::SocketAddr; // The `CockroachAdminFromBlueprintViaFixedPort` type above is the standard @@ -260,109 +258,83 @@ mod tests { // can _write_ that test), so test it in isolation here. #[test] fn test_default_cockroach_admin_addrs_from_blueprint() { - // Construct an empty blueprint with one sled. - let sled_id = SledUuid::new_v4(); - let mut blueprint = BlueprintBuilder::build_empty_with_sleds( - iter::once(sled_id), - "test", - ); - let bp_sled = &mut blueprint - .sleds - .get_mut(&sled_id) - .expect("found entry for test sled"); - - let zpool_id = ZpoolUuid::new_v4(); - let make_crdb_zone_config = - |disposition, id, addr: SocketAddrV6| BlueprintZoneConfig { - disposition, - id, - filesystem_pool: ZpoolName::new_external(zpool_id), - zone_type: BlueprintZoneType::CockroachDb( - blueprint_zone_type::CockroachDb { - address: addr, - dataset: OmicronZoneDataset { - pool_name: format!("oxp_{}", zpool_id) - .parse() - .unwrap(), - }, - }, - ), - image_source: BlueprintZoneImageSource::InstallDataset, - }; - - // Add three CRDB zones with known addresses; the first and third are - // in service, and the second is expunged. Only the first and third - // should show up when we ask for addresses below. - let crdb_id1 = OmicronZoneUuid::new_v4(); - let crdb_id2 = OmicronZoneUuid::new_v4(); - let crdb_id3 = OmicronZoneUuid::new_v4(); - let crdb_addr1: SocketAddrV6 = "[2001:db8::1]:1111".parse().unwrap(); - let crdb_addr2: SocketAddrV6 = "[2001:db8::2]:1234".parse().unwrap(); - let crdb_addr3: SocketAddrV6 = "[2001:db8::3]:1234".parse().unwrap(); - bp_sled - .zones - .insert_unique(make_crdb_zone_config( - BlueprintZoneDisposition::InService, - crdb_id1, - crdb_addr1, - )) - .expect("freshly generated zone IDs are unique"); - bp_sled - .zones - .insert_unique(make_crdb_zone_config( - BlueprintZoneDisposition::Expunged { + const TEST_NAME: &str = + "test_default_cockroach_admin_addrs_from_blueprint"; + let logctx = dev::test_setup_log(TEST_NAME); + let log = &logctx.log; + + // Build an example system with one sled. + let (example_system, bp0) = + ExampleSystemBuilder::new(log, TEST_NAME).nsleds(1).build(); + let input = example_system.input; + + // `ExampleSystemBuilder` doesn't place any cockroach nodes; assert so + // we bail out early if that changes. + let ncockroach = bp0 + .all_omicron_zones(BlueprintZoneDisposition::is_in_service) + .filter(|(_, z)| z.zone_type.is_cockroach()) + .count(); + assert_eq!(ncockroach, 0); + + // This blueprint has no cockroach zones, so should have no admin addrs + // either. + let admin_addrs = CockroachAdminFromBlueprintViaFixedPort + .cockroach_admin_addrs(&bp0) + .collect::>(); + assert_eq!(admin_addrs, Vec::new()); + + // Add 5 cockroach zones to our sled. + let mut builder = BlueprintBuilder::new_based_on( + log, + &bp0, + &input, + TEST_NAME, + PlannerRng::from_entropy(), + ) + .expect("constructed builder"); + let sled_id = bp0.sleds().next().expect("1 sled"); + for _ in 0..5 { + builder + .sled_add_zone_cockroachdb( + sled_id, + BlueprintZoneImageSource::InstallDataset, + ) + .expect("added cockroach"); + } + let mut bp1 = builder.build(BlueprintSource::Test); + + // Mutate this blueprint: expunge 2 of the 5 zones. Record the expected + // admin addrs from the other three. + let mut expected = BTreeSet::new(); + let sled_config = bp1.sleds.get_mut(&sled_id).unwrap(); + let mut seen = 0; + for mut zone in sled_config.zones.iter_mut() { + if !zone.zone_type.is_cockroach() { + continue; + } + // Keep even; expunge odd. + if seen % 2 == 0 { + let ip = zone.underlay_ip(); + let addr = SocketAddrV6::new(ip, COCKROACH_ADMIN_PORT, 0, 0); + expected.insert((zone.id, addr)); + } else { + zone.disposition = BlueprintZoneDisposition::Expunged { as_of_generation: Generation::new(), ready_for_cleanup: false, - }, - crdb_id2, - crdb_addr2, - )) - .expect("freshly generated zone IDs are unique"); - bp_sled - .zones - .insert_unique(make_crdb_zone_config( - BlueprintZoneDisposition::InService, - crdb_id3, - crdb_addr3, - )) - .expect("freshly generated zone IDs are unique"); - - // Also add a non-CRDB zone to ensure it's filtered out. - bp_sled - .zones - .insert_unique(BlueprintZoneConfig { - disposition: BlueprintZoneDisposition::InService, - id: OmicronZoneUuid::new_v4(), - filesystem_pool: ZpoolName::new_external(ZpoolUuid::new_v4()), - zone_type: BlueprintZoneType::CruciblePantry( - blueprint_zone_type::CruciblePantry { - address: "[::1]:0".parse().unwrap(), - }, - ), - image_source: BlueprintZoneImageSource::InstallDataset, - }) - .expect("freshly generated zone IDs are unique"); - - // We expect to see CRDB zones 1 and 3 with their IPs but the ports - // changed to `COCKROACH_ADMIN_PORT`. - let mut expected = vec![ - ( - crdb_id1, - SocketAddrV6::new(*crdb_addr1.ip(), COCKROACH_ADMIN_PORT, 0, 0), - ), - ( - crdb_id3, - SocketAddrV6::new(*crdb_addr3.ip(), COCKROACH_ADMIN_PORT, 0, 0), - ), - ]; - // We sort starting with zone id, since the original zones are sorted - // that way in a map. - expected.sort_unstable(); + }; + } + seen += 1; + } + assert_eq!(seen, 5); + assert_eq!(expected.len(), 3); + // Confirm we only see the three expected addrs. let admin_addrs = CockroachAdminFromBlueprintViaFixedPort - .cockroach_admin_addrs(&blueprint) - .collect::>(); + .cockroach_admin_addrs(&bp1) + .collect::>(); assert_eq!(expected, admin_addrs); + + logctx.cleanup_successful(); } #[tokio::test] From 9c30cd7f73597e8a5a94fad87868123754fbbc15 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 12 Nov 2025 16:50:47 -0500 Subject: [PATCH 14/24] remove build_empty_with_sleds() --- .../planning/src/blueprint_builder/builder.rs | 62 ++----------------- 1 file changed, 6 insertions(+), 56 deletions(-) diff --git a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs index 8f189f66673..44bb29207ac 100644 --- a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs +++ b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs @@ -33,7 +33,6 @@ use nexus_types::deployment::BlueprintHostPhase2DesiredContents; use nexus_types::deployment::BlueprintHostPhase2DesiredSlots; use nexus_types::deployment::BlueprintPhysicalDiskConfig; use nexus_types::deployment::BlueprintPhysicalDiskDisposition; -use nexus_types::deployment::BlueprintSledConfig; use nexus_types::deployment::BlueprintSource; use nexus_types::deployment::BlueprintZoneConfig; use nexus_types::deployment::BlueprintZoneDisposition; @@ -486,9 +485,8 @@ struct OximeterReadPolicy { /// /// 1. Build one directly. This would generally only be used once in the /// lifetime of a rack, to assemble the first blueprint during rack setup. -/// It is also common in tests. To start with a blueprint that contains an -/// empty zone config for some number of sleds, use -/// [`BlueprintBuilder::build_empty_with_sleds`]. +/// It is also common in tests. To start with an empty initial blueprint, +/// use [`BlueprintBuilder::build_empty`]. /// /// 2. Build one _from_ another blueprint, called the "parent", making changes /// as desired. Use [`BlueprintBuilder::new_based_on`] for this. Once the @@ -533,64 +531,16 @@ impl<'a> BlueprintBuilder<'a> { /// Directly construct an empty blueprint: no sleds; default values for all /// other fields. pub fn build_empty(creator: &str) -> Blueprint { - Self::build_empty_with_sleds(iter::empty(), creator) + Self::build_empty_seeded(creator, PlannerRng::from_entropy()) } /// A version of [`Self::build_empty`] that allows the /// blueprint ID to be generated from a deterministic RNG. - pub fn build_empty_seeded(creator: &str, rng: PlannerRng) -> Blueprint { - Self::build_empty_with_sleds_seeded(iter::empty(), creator, rng) - } - - /// Directly construct a `Blueprint` that contains an empty zone config for - /// the given sleds. - pub fn build_empty_with_sleds( - sled_ids: impl Iterator, - creator: &str, - ) -> Blueprint { - Self::build_empty_with_sleds_impl( - sled_ids, - creator, - PlannerRng::from_entropy(), - ) - } - - /// A version of [`Self::build_empty_with_sleds`] that allows the - /// blueprint ID to be generated from a deterministic RNG. - pub fn build_empty_with_sleds_seeded( - sled_ids: impl Iterator, - creator: &str, - rng: PlannerRng, - ) -> Blueprint { - Self::build_empty_with_sleds_impl(sled_ids, creator, rng) - } - - fn build_empty_with_sleds_impl( - sled_ids: impl Iterator, - creator: &str, - mut rng: PlannerRng, - ) -> Blueprint { - let sleds = sled_ids - .map(|sled_id| { - let config = BlueprintSledConfig { - state: SledState::Active, - sled_agent_generation: Generation::new(), - disks: IdOrdMap::default(), - datasets: IdOrdMap::default(), - zones: IdOrdMap::default(), - remove_mupdate_override: None, - host_phase_2: - BlueprintHostPhase2DesiredSlots::current_contents(), - }; - (sled_id, config) - }) - .collect::>(); - let num_sleds = sleds.len(); - + pub fn build_empty_seeded(creator: &str, mut rng: PlannerRng) -> Blueprint { let id = rng.next_blueprint(); Blueprint { id, - sleds, + sleds: BTreeMap::new(), pending_mgs_updates: PendingMgsUpdates::new(), parent_blueprint_id: None, internal_dns_version: Generation::new(), @@ -605,7 +555,7 @@ impl<'a> BlueprintBuilder<'a> { oximeter_read_mode: OximeterReadMode::SingleNode, time_created: now_db_precision(), creator: creator.to_owned(), - comment: format!("starting blueprint with {num_sleds} empty sleds"), + comment: format!("starting blueprint (empty)"), // The only reason to create empty blueprints is tests. If that // changes (e.g., if RSS starts using this builder to generate its // blueprints), we could take a `source` argument instead. From de048ec9af32f54ca8c2f28ae4c62f89f02ab6a4 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 12 Nov 2025 16:55:01 -0500 Subject: [PATCH 15/24] expectorate --- .../tests/output/cmds-blueprint-history-stdout | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-blueprint-history-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-blueprint-history-stdout index 72ec7dd7aab..ea19fbdcd47 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-blueprint-history-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-blueprint-history-stdout @@ -26,7 +26,7 @@ blueprint df06bb57-ad42-4431-9206-abff322896c7 created from blueprint af934083-5 > # which does not include any of those blueprints. > blueprint-history TIME BLUEPRINT - 184f10b3-61cb-41ef-9b93-3489b2bac559 starting blueprint with 0 empty sleds + 184f10b3-61cb-41ef-9b93-3489b2bac559 starting blueprint (empty) dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 @@ -34,7 +34,7 @@ TIME BLUEPRINT > # You can give it a specific blueprint. > blueprint-history 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 TIME BLUEPRINT - 184f10b3-61cb-41ef-9b93-3489b2bac559 starting blueprint with 0 empty sleds + 184f10b3-61cb-41ef-9b93-3489b2bac559 starting blueprint (empty) dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 @@ -43,7 +43,7 @@ TIME BLUEPRINT > # Running it from the latest blueprint should report all of them. > blueprint-history latest TIME BLUEPRINT - 184f10b3-61cb-41ef-9b93-3489b2bac559 starting blueprint with 0 empty sleds + 184f10b3-61cb-41ef-9b93-3489b2bac559 starting blueprint (empty) dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 58d5e830-0884-47d8-a7cd-b2b3751adeb4 @@ -64,7 +64,7 @@ TIME BLUEPRINT > # Show diffs, too. > blueprint-history --diff latest TIME BLUEPRINT - 184f10b3-61cb-41ef-9b93-3489b2bac559 starting blueprint with 0 empty sleds + 184f10b3-61cb-41ef-9b93-3489b2bac559 starting blueprint (empty) dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 from: blueprint 184f10b3-61cb-41ef-9b93-3489b2bac559 to: blueprint dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 From 263cd1f4026cbd1400153a01098f1cf3ee88167c Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 12 Nov 2025 17:06:53 -0500 Subject: [PATCH 16/24] clippy --- nexus/reconfigurator/planning/src/blueprint_builder/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs index 44bb29207ac..ca9c8c67480 100644 --- a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs +++ b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs @@ -555,7 +555,7 @@ impl<'a> BlueprintBuilder<'a> { oximeter_read_mode: OximeterReadMode::SingleNode, time_created: now_db_precision(), creator: creator.to_owned(), - comment: format!("starting blueprint (empty)"), + comment: "starting blueprint (empty)".to_string(), // The only reason to create empty blueprints is tests. If that // changes (e.g., if RSS starts using this builder to generate its // blueprints), we could take a `source` argument instead. From c2e6bedc7db0655e260377639662789c7292d310 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 17 Nov 2025 13:17:33 -0500 Subject: [PATCH 17/24] add sled subnet to blueprint (planner tests pass) --- common/src/address.rs | 8 +++ .../planning/src/blueprint_builder/builder.rs | 29 ++--------- .../src/blueprint_editor/sled_editor.rs | 52 ++++++++----------- .../example_builder_zone_counts_blueprint.txt | 25 +++++++-- .../planner_decommissions_sleds_bp2.txt | 15 ++++-- .../output/planner_nonprovisionable_bp2.txt | 25 +++++++-- nexus/types/src/deployment.rs | 17 +++--- .../types/src/deployment/blueprint_display.rs | 3 ++ 8 files changed, 100 insertions(+), 74 deletions(-) diff --git a/common/src/address.rs b/common/src/address.rs index 8dee236c259..20e5f8bbca3 100644 --- a/common/src/address.rs +++ b/common/src/address.rs @@ -9,6 +9,7 @@ use crate::api::external::{self, Error}; use crate::policy::INTERNAL_DNS_REDUNDANCY; +use daft::Diffable; use ipnetwork::Ipv6Network; use oxnet::{Ipv4Net, Ipv6Net}; use schemars::JsonSchema; @@ -273,12 +274,19 @@ pub const SLED_RESERVED_ADDRESSES: u16 = 32; Eq, PartialOrd, Ord, + Diffable, )] #[schemars(rename = "Ipv6Subnet")] pub struct Ipv6Subnet { net: Ipv6Net, } +impl std::fmt::Display for Ipv6Subnet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.net.fmt(f) + } +} + impl Ipv6Subnet { pub fn new(addr: Ipv6Addr) -> Self { // Create a network with the compile-time prefix length. diff --git a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs index ca9c8c67480..35023d421fb 100644 --- a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs +++ b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs @@ -580,31 +580,10 @@ impl<'a> BlueprintBuilder<'a> { // Convert our parent blueprint's sled configs into `SledEditor`s. let mut sled_editors = BTreeMap::new(); for (sled_id, sled_cfg) in &parent_blueprint.sleds { - let state = sled_cfg.state; - - let editor = match state { - SledState::Active => { - let details = input - .sled_lookup(SledFilter::Commissioned, *sled_id) - .with_context(|| { - format!( - "failed to find sled details for \ - active sled in parent blueprint {sled_id}" - ) - })?; - SledEditor::for_existing_active( - details.resources.subnet, - sled_cfg.clone(), - ) - } - SledState::Decommissioned => { - SledEditor::for_existing_decommissioned(sled_cfg.clone()) - } - } - .with_context(|| { - format!("failed to construct SledEditor for sled {sled_id}") - })?; - + let editor = SledEditor::for_existing(sled_cfg.clone()) + .with_context(|| { + format!("failed to construct SledEditor for sled {sled_id}") + })?; sled_editors.insert(*sled_id, editor); } diff --git a/nexus/reconfigurator/planning/src/blueprint_editor/sled_editor.rs b/nexus/reconfigurator/planning/src/blueprint_editor/sled_editor.rs index a88f59b6315..e0a75b1b367 100644 --- a/nexus/reconfigurator/planning/src/blueprint_editor/sled_editor.rs +++ b/nexus/reconfigurator/planning/src/blueprint_editor/sled_editor.rs @@ -159,33 +159,18 @@ enum InnerSledEditor { } impl SledEditor { - pub fn for_existing_active( - subnet: Ipv6Subnet, + pub fn for_existing( config: BlueprintSledConfig, ) -> Result { - assert_eq!( - config.state, - SledState::Active, - "for_existing_active called on non-active sled" - ); - let inner = ActiveSledEditor::new(subnet, config)?; - Ok(Self(InnerSledEditor::Active(inner))) - } - - pub fn for_existing_decommissioned( - config: BlueprintSledConfig, - ) -> Result { - assert_eq!( - config.state, - SledState::Decommissioned, - "for_existing_decommissioned called on non-decommissioned sled" - ); - let inner = EditedSled { - config, - edit_counts: SledEditCounts::zeroes(), - scalar_edits: EditedSledScalarEdits::zeroes(), + let inner = match config.state { + SledState::Active => { + InnerSledEditor::Active(ActiveSledEditor::new(config)?) + } + SledState::Decommissioned => { + InnerSledEditor::Decommissioned(EditedSled::new(config)) + } }; - Ok(Self(InnerSledEditor::Decommissioned(inner))) + Ok(Self(inner)) } pub fn for_new_active(subnet: Ipv6Subnet) -> Self { @@ -504,11 +489,18 @@ pub(crate) struct EditedSled { pub scalar_edits: EditedSledScalarEdits, } +impl EditedSled { + fn new(config: BlueprintSledConfig) -> Self { + Self { + config, + edit_counts: SledEditCounts::zeroes(), + scalar_edits: EditedSledScalarEdits::zeroes(), + } + } +} + impl ActiveSledEditor { - pub fn new( - subnet: Ipv6Subnet, - config: BlueprintSledConfig, - ) -> Result { + pub fn new(config: BlueprintSledConfig) -> Result { let zones = ZonesEditor::new(config.sled_agent_generation, config.zones); @@ -521,7 +513,8 @@ impl ActiveSledEditor { Ok(Self { underlay_ip_allocator: SledUnderlayIpAllocator::new( - subnet, zone_ips, + config.subnet, + zone_ips, ), incoming_sled_agent_generation: config.sled_agent_generation, zones, @@ -585,6 +578,7 @@ impl ActiveSledEditor { EditedSled { config: BlueprintSledConfig { state: SledState::Active, + subnet: self.underlay_ip_allocator.subnet(), sled_agent_generation, disks, datasets, diff --git a/nexus/reconfigurator/planning/tests/output/example_builder_zone_counts_blueprint.txt b/nexus/reconfigurator/planning/tests/output/example_builder_zone_counts_blueprint.txt index cd6ce50710c..585009ab08c 100644 --- a/nexus/reconfigurator/planning/tests/output/example_builder_zone_counts_blueprint.txt +++ b/nexus/reconfigurator/planning/tests/output/example_builder_zone_counts_blueprint.txt @@ -1,7 +1,10 @@ blueprint 4a0b8410-b14f-41e7-85e7-3c0fe7050ccc parent: e35b2fdd-354d-48d9-acb5-703b2c269a54 - sled: 0dbf1e39-e265-4071-a8df-6d1225b46694 (active, config generation 2) + sled: 0dbf1e39-e265-4071-a8df-6d1225b46694 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:105::/64 host phase 2 contents: ------------------------ @@ -120,7 +123,10 @@ parent: e35b2fdd-354d-48d9-acb5-703b2c269a54 - sled: 15cf73a6-445b-4d36-9232-5ed364019bc6 (active, config generation 2) + sled: 15cf73a6-445b-4d36-9232-5ed364019bc6 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:104::/64 host phase 2 contents: ------------------------ @@ -234,7 +240,10 @@ parent: e35b2fdd-354d-48d9-acb5-703b2c269a54 - sled: 50e6c1c0-43b2-4abc-9041-41165597f639 (active, config generation 2) + sled: 50e6c1c0-43b2-4abc-9041-41165597f639 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -345,7 +354,10 @@ parent: e35b2fdd-354d-48d9-acb5-703b2c269a54 - sled: 969ff976-df34-402c-a362-53db03a6b97f (active, config generation 2) + sled: 969ff976-df34-402c-a362-53db03a6b97f + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ @@ -456,7 +468,10 @@ parent: e35b2fdd-354d-48d9-acb5-703b2c269a54 - sled: ec5c0b37-b651-4c45-ac1c-24541ef9c44b (active, config generation 2) + sled: ec5c0b37-b651-4c45-ac1c-24541ef9c44b + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ diff --git a/nexus/reconfigurator/planning/tests/output/planner_decommissions_sleds_bp2.txt b/nexus/reconfigurator/planning/tests/output/planner_decommissions_sleds_bp2.txt index 09c4543f386..869c520946d 100644 --- a/nexus/reconfigurator/planning/tests/output/planner_decommissions_sleds_bp2.txt +++ b/nexus/reconfigurator/planning/tests/output/planner_decommissions_sleds_bp2.txt @@ -1,7 +1,10 @@ blueprint 1ac2d88f-27dd-4506-8585-6b2be832528e parent: 516e80a3-b362-4fac-bd3c-4559717120dd - sled: a1b477db-b629-48eb-911d-1ccdafca75b9 (decommissioned, config generation 3) + sled: a1b477db-b629-48eb-911d-1ccdafca75b9 + state::::::::::::: decommissioned + config generation: 3 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ @@ -112,7 +115,10 @@ parent: 516e80a3-b362-4fac-bd3c-4559717120dd - sled: d67ce8f0-a691-4010-b414-420d82e80527 (active, config generation 3) + sled: d67ce8f0-a691-4010-b414-420d82e80527 + state::::::::::::: active + config generation: 3 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -224,7 +230,10 @@ parent: 516e80a3-b362-4fac-bd3c-4559717120dd - sled: fefcf4cf-f7e7-46b3-b629-058526ce440e (active, config generation 3) + sled: fefcf4cf-f7e7-46b3-b629-058526ce440e + state::::::::::::: active + config generation: 3 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ diff --git a/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_bp2.txt b/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_bp2.txt index e4e0d1907ce..96db5496f0c 100644 --- a/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_bp2.txt +++ b/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_bp2.txt @@ -1,7 +1,10 @@ blueprint 9f71f5d3-a272-4382-9154-6ea2e171a6c6 parent: 4d4e6c38-cd95-4c4e-8f45-6af4d686964b - sled: 2d1cb4f2-cf44-40fc-b118-85036eb732a9 (active, config generation 2) + sled: 2d1cb4f2-cf44-40fc-b118-85036eb732a9 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:105::/64 host phase 2 contents: ------------------------ @@ -112,7 +115,10 @@ parent: 4d4e6c38-cd95-4c4e-8f45-6af4d686964b - sled: 48d95fef-bc9f-4f50-9a53-1e075836291d (decommissioned, config generation 3) + sled: 48d95fef-bc9f-4f50-9a53-1e075836291d + state::::::::::::: decommissioned + config generation: 3 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ @@ -220,7 +226,10 @@ parent: 4d4e6c38-cd95-4c4e-8f45-6af4d686964b - sled: 68d24ac5-f341-49ea-a92a-0381b52ab387 (decommissioned, config generation 2) + sled: 68d24ac5-f341-49ea-a92a-0381b52ab387 + state::::::::::::: decommissioned + config generation: 2 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -328,7 +337,10 @@ parent: 4d4e6c38-cd95-4c4e-8f45-6af4d686964b - sled: 75bc286f-2b4b-482c-9431-59272af529da (active, config generation 3) + sled: 75bc286f-2b4b-482c-9431-59272af529da + state::::::::::::: active + config generation: 3 + subnet:::::::::::: fd00:1122:3344:104::/64 host phase 2 contents: ------------------------ @@ -437,7 +449,10 @@ parent: 4d4e6c38-cd95-4c4e-8f45-6af4d686964b - sled: affab35f-600a-4109-8ea0-34a067a4e0bc (active, config generation 3) + sled: affab35f-600a-4109-8ea0-34a067a4e0bc + state::::::::::::: active + config generation: 3 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ diff --git a/nexus/types/src/deployment.rs b/nexus/types/src/deployment.rs index 01272eeb750..2cee0ca6e26 100644 --- a/nexus/types/src/deployment.rs +++ b/nexus/types/src/deployment.rs @@ -34,6 +34,8 @@ use nexus_sled_agent_shared::inventory::OmicronSledConfig; use nexus_sled_agent_shared::inventory::OmicronZoneConfig; use nexus_sled_agent_shared::inventory::OmicronZoneImageSource; use nexus_sled_agent_shared::inventory::ZoneKind; +use omicron_common::address::Ipv6Subnet; +use omicron_common::address::SLED_PREFIX; use omicron_common::api::external::ByteCount; use omicron_common::api::external::Generation; use omicron_common::api::external::TufArtifactMeta; @@ -814,6 +816,7 @@ impl fmt::Display for BlueprintDisplay<'_> { for (sled_id, config) in sleds { let BlueprintSledConfig { state, + subnet, sled_agent_generation, disks, datasets, @@ -822,14 +825,13 @@ impl fmt::Display for BlueprintDisplay<'_> { host_phase_2, } = config; - // Report the sled state - writeln!( - f, - "\n sled: {sled_id} ({state}, config generation \ - {sled_agent_generation})", - )?; - + // Report toplevel sled info + writeln!(f, "\n sled: {sled_id}")?; let mut rows = Vec::new(); + rows.push((STATE, state.to_string())); + rows.push((CONFIG_GENERATION, sled_agent_generation.to_string())); + rows.push((SUBNET, subnet.to_string())); + if let Some(id) = remove_mupdate_override { rows.push((WILL_REMOVE_MUPDATE_OVERRIDE, id.to_string())); } @@ -926,6 +928,7 @@ impl fmt::Display for BlueprintDisplay<'_> { )] pub struct BlueprintSledConfig { pub state: SledState, + pub subnet: Ipv6Subnet, /// Generation number used when this type is converted into an /// `OmicronSledConfig` for use by sled-agent. diff --git a/nexus/types/src/deployment/blueprint_display.rs b/nexus/types/src/deployment/blueprint_display.rs index dec9ce3e699..bfa51da9fcb 100644 --- a/nexus/types/src/deployment/blueprint_display.rs +++ b/nexus/types/src/deployment/blueprint_display.rs @@ -53,6 +53,9 @@ pub mod constants { "(not present in collection)"; pub const INVALID_VALUE_PARENS: &str = "(invalid value)"; pub const GENERATION: &str = "generation"; + pub const STATE: &str = "state"; + pub const CONFIG_GENERATION: &str = "config generation"; + pub const SUBNET: &str = "subnet"; } use constants::*; use std::fmt::Display; From f33e983d0ee1351d3c6a3a527fcbfbd50a35fb68 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 17 Nov 2025 13:19:28 -0500 Subject: [PATCH 18/24] expectorate: reconfigurator-cli tests --- .../output/cmds-add-sled-no-disks-stdout | 20 ++++- .../tests/output/cmds-example-stdout | 35 +++++++-- ...ds-expunge-newly-added-external-dns-stdout | 45 ++++++++--- ...ds-expunge-newly-added-internal-dns-stdout | 15 +++- .../tests/output/cmds-expunge-zones-stdout | 15 +++- .../output/cmds-host-phase-2-source-stdout | 10 ++- .../output/cmds-mupdate-update-flow-stdout | 45 ++++++++--- .../cmds-nexus-generation-autobump-stdout | 15 +++- .../tests/output/cmds-set-mgs-updates-stdout | 75 +++++++++++++++---- .../cmds-set-remove-mupdate-override-stdout | 70 +++++++++++++---- .../tests/output/cmds-set-zone-images-stdout | 15 +++- .../tests/output/cmds-target-release-stdout | 15 +++- .../tests/output/cmds-unsafe-zone-mgs-stdout | 15 +++- 13 files changed, 312 insertions(+), 78 deletions(-) diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-add-sled-no-disks-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-add-sled-no-disks-stdout index 19e1a7a3cf0..0e7584d3ea5 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-add-sled-no-disks-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-add-sled-no-disks-stdout @@ -57,7 +57,10 @@ planning report: blueprint 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 parent: dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 - sled: 00320471-945d-413c-85e7-03e091a70b3c (active, config generation 1) + sled: 00320471-945d-413c-85e7-03e091a70b3c + state::::::::::::: active + config generation: 1 + subnet:::::::::::: fd00:1122:3344:104::/64 host phase 2 contents: ------------------------ @@ -86,7 +89,10 @@ parent: dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 - sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c (active, config generation 2) + sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -151,7 +157,10 @@ parent: dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 - sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (active, config generation 2) + sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -213,7 +222,10 @@ parent: dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 - sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 (active, config generation 2) + sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-example-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-example-stdout index a3028f9b689..847f19f00b7 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-example-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-example-stdout @@ -82,7 +82,10 @@ zpools (10): blueprint ade5749d-bdf3-4fab-a8ae-00bea01b3a5a parent: 02697f74-b14a-4418-90f0-c28b2a3a6aa9 - sled: 2eb69596-f081-4e2d-9425-9994926e0832 (active, config generation 2) + sled: 2eb69596-f081-4e2d-9425-9994926e0832 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -196,7 +199,10 @@ parent: 02697f74-b14a-4418-90f0-c28b2a3a6aa9 - sled: 32d8d836-4d8a-4e54-8fa9-f31d79c42646 (active, config generation 2) + sled: 32d8d836-4d8a-4e54-8fa9-f31d79c42646 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ @@ -307,7 +313,10 @@ parent: 02697f74-b14a-4418-90f0-c28b2a3a6aa9 - sled: 89d02b1b-478c-401a-8e28-7a26f74fa41b (active, config generation 2) + sled: 89d02b1b-478c-401a-8e28-7a26f74fa41b + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -497,7 +506,10 @@ zpools (4): blueprint ade5749d-bdf3-4fab-a8ae-00bea01b3a5a parent: 02697f74-b14a-4418-90f0-c28b2a3a6aa9 - sled: 89d02b1b-478c-401a-8e28-7a26f74fa41b (active, config generation 2) + sled: 89d02b1b-478c-401a-8e28-7a26f74fa41b + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -1151,7 +1163,10 @@ T ENA ID PARENT blueprint ade5749d-bdf3-4fab-a8ae-00bea01b3a5a parent: 02697f74-b14a-4418-90f0-c28b2a3a6aa9 - sled: 2eb69596-f081-4e2d-9425-9994926e0832 (active, config generation 2) + sled: 2eb69596-f081-4e2d-9425-9994926e0832 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -1203,7 +1218,10 @@ parent: 02697f74-b14a-4418-90f0-c28b2a3a6aa9 - sled: 32d8d836-4d8a-4e54-8fa9-f31d79c42646 (active, config generation 2) + sled: 32d8d836-4d8a-4e54-8fa9-f31d79c42646 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ @@ -1255,7 +1273,10 @@ parent: 02697f74-b14a-4418-90f0-c28b2a3a6aa9 - sled: 89d02b1b-478c-401a-8e28-7a26f74fa41b (active, config generation 2) + sled: 89d02b1b-478c-401a-8e28-7a26f74fa41b + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-external-dns-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-external-dns-stdout index 5b6455a499e..567030c7ba4 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-external-dns-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-external-dns-stdout @@ -11,7 +11,10 @@ loaded example system with: blueprint 3f00b694-1b16-4aaa-8f78-e6b3a527b434 parent: 06c88262-f435-410e-ba98-101bed41ec27 - sled: 711ac7f8-d19e-4572-bdb9-e9b50f6e362a (active, config generation 2) + sled: 711ac7f8-d19e-4572-bdb9-e9b50f6e362a + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ @@ -125,7 +128,10 @@ parent: 06c88262-f435-410e-ba98-101bed41ec27 - sled: 9dc50690-f9bf-4520-bf80-051d0f465c2c (active, config generation 2) + sled: 9dc50690-f9bf-4520-bf80-051d0f465c2c + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -236,7 +242,10 @@ parent: 06c88262-f435-410e-ba98-101bed41ec27 - sled: a88790de-5962-4871-8686-61c1fd5b7094 (active, config generation 2) + sled: a88790de-5962-4871-8686-61c1fd5b7094 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -549,7 +558,10 @@ external DNS: blueprint 366b0b68-d80e-4bc1-abd3-dc69837847e0 parent: 3f00b694-1b16-4aaa-8f78-e6b3a527b434 - sled: 711ac7f8-d19e-4572-bdb9-e9b50f6e362a (active, config generation 3) + sled: 711ac7f8-d19e-4572-bdb9-e9b50f6e362a + state::::::::::::: active + config generation: 3 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ @@ -663,7 +675,10 @@ parent: 3f00b694-1b16-4aaa-8f78-e6b3a527b434 - sled: 9dc50690-f9bf-4520-bf80-051d0f465c2c (active, config generation 2) + sled: 9dc50690-f9bf-4520-bf80-051d0f465c2c + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -774,7 +789,10 @@ parent: 3f00b694-1b16-4aaa-8f78-e6b3a527b434 - sled: a88790de-5962-4871-8686-61c1fd5b7094 (active, config generation 2) + sled: a88790de-5962-4871-8686-61c1fd5b7094 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -1112,7 +1130,10 @@ external DNS: blueprint 9c998c1d-1a7b-440a-ae0c-40f781dea6e2 parent: 366b0b68-d80e-4bc1-abd3-dc69837847e0 - sled: 711ac7f8-d19e-4572-bdb9-e9b50f6e362a (active, config generation 4) + sled: 711ac7f8-d19e-4572-bdb9-e9b50f6e362a + state::::::::::::: active + config generation: 4 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ @@ -1229,7 +1250,10 @@ parent: 366b0b68-d80e-4bc1-abd3-dc69837847e0 - sled: 9dc50690-f9bf-4520-bf80-051d0f465c2c (active, config generation 2) + sled: 9dc50690-f9bf-4520-bf80-051d0f465c2c + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -1340,7 +1364,10 @@ parent: 366b0b68-d80e-4bc1-abd3-dc69837847e0 - sled: a88790de-5962-4871-8686-61c1fd5b7094 (active, config generation 2) + sled: a88790de-5962-4871-8686-61c1fd5b7094 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-internal-dns-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-internal-dns-stdout index 52f5192d2cf..8d8f96928c4 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-internal-dns-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-internal-dns-stdout @@ -9,7 +9,10 @@ loaded example system with: blueprint dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 parent: 184f10b3-61cb-41ef-9b93-3489b2bac559 - sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c (active, config generation 2) + sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -123,7 +126,10 @@ parent: 184f10b3-61cb-41ef-9b93-3489b2bac559 - sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (active, config generation 2) + sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -234,7 +240,10 @@ parent: 184f10b3-61cb-41ef-9b93-3489b2bac559 - sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 (active, config generation 2) + sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-zones-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-zones-stdout index 1ad4c319a79..213417a4d10 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-zones-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-zones-stdout @@ -14,7 +14,10 @@ T ENA ID PARENT blueprint dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 parent: 184f10b3-61cb-41ef-9b93-3489b2bac559 - sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c (active, config generation 2) + sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -79,7 +82,10 @@ parent: 184f10b3-61cb-41ef-9b93-3489b2bac559 - sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (active, config generation 2) + sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -141,7 +147,10 @@ parent: 184f10b3-61cb-41ef-9b93-3489b2bac559 - sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 (active, config generation 2) + sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-host-phase-2-source-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-host-phase-2-source-stdout index 612002675cb..45933052308 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-host-phase-2-source-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-host-phase-2-source-stdout @@ -353,7 +353,10 @@ external DNS: blueprint 58d5e830-0884-47d8-a7cd-b2b3751adeb4 parent: 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 - sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (active, config generation 4) + sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 + state::::::::::::: active + config generation: 4 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------------ @@ -856,7 +859,10 @@ external DNS: blueprint df06bb57-ad42-4431-9206-abff322896c7 parent: af934083-59b5-4bf6-8966-6fb5292c29e1 - sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (active, config generation 6) + sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 + state::::::::::::: active + config generation: 6 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-mupdate-update-flow-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-mupdate-update-flow-stdout index 1033d599d0c..7c8f6ddf074 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-mupdate-update-flow-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-mupdate-update-flow-stdout @@ -1218,7 +1218,10 @@ planning report: blueprint afb09faf-a586-4483-9289-04d4f1d8ba23 parent: c1a0d242-9160-40f4-96ae-61f8f40a0b1b - sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c (active, config generation 5) + sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c + state::::::::::::: active + config generation: 5 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -1269,7 +1272,10 @@ parent: c1a0d242-9160-40f4-96ae-61f8f40a0b1b - sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (active, config generation 7) + sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 + state::::::::::::: active + config generation: 7 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -1317,7 +1323,10 @@ parent: c1a0d242-9160-40f4-96ae-61f8f40a0b1b - sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 (active, config generation 5) + sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 + state::::::::::::: active + config generation: 5 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ @@ -1548,7 +1557,10 @@ planning report: blueprint ce365dff-2cdb-4f35-a186-b15e20e1e700 parent: afb09faf-a586-4483-9289-04d4f1d8ba23 - sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c (active, config generation 5) + sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c + state::::::::::::: active + config generation: 5 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -1599,7 +1611,10 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 - sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (active, config generation 7) + sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 + state::::::::::::: active + config generation: 7 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -1647,7 +1662,10 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 - sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 (active, config generation 6) + sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 + state::::::::::::: active + config generation: 6 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ @@ -1914,7 +1932,10 @@ planning report: blueprint 12d602a6-5ab4-487a-b94e-eb30cdf30300 parent: 8f2d1f39-7c88-4701-aa43-56bf281b28c1 - sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c (active, config generation 6) + sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c + state::::::::::::: active + config generation: 6 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -1965,7 +1986,10 @@ parent: 8f2d1f39-7c88-4701-aa43-56bf281b28c1 - sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (active, config generation 7) + sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 + state::::::::::::: active + config generation: 7 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -2013,7 +2037,10 @@ parent: 8f2d1f39-7c88-4701-aa43-56bf281b28c1 - sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 (active, config generation 6) + sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 + state::::::::::::: active + config generation: 6 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-nexus-generation-autobump-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-nexus-generation-autobump-stdout index 0b75dfb0291..8cdec0d67bf 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-nexus-generation-autobump-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-nexus-generation-autobump-stdout @@ -1072,7 +1072,10 @@ empty planning report blueprint 58d5e830-0884-47d8-a7cd-b2b3751adeb4 parent: 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 - sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c (active, config generation 3) + sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c + state::::::::::::: active + config generation: 3 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------------ @@ -1137,7 +1140,10 @@ parent: 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 - sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (active, config generation 3) + sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 + state::::::::::::: active + config generation: 3 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------------ @@ -1199,7 +1205,10 @@ parent: 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 - sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 (active, config generation 3) + sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 + state::::::::::::: active + config generation: 3 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------------ diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-set-mgs-updates-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-set-mgs-updates-stdout index 0f500bfd07d..dce677037c7 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-set-mgs-updates-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-set-mgs-updates-stdout @@ -11,7 +11,10 @@ loaded example system with: blueprint ad97e762-7bf1-45a6-a98f-60afb7e491c0 parent: 6ccc786b-17f1-4562-958f-5a7d9a5a15fd - sled: bb0ec23a-f97c-4b6a-a5bc-864b1ebc9236 (active, config generation 2) + sled: bb0ec23a-f97c-4b6a-a5bc-864b1ebc9236 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -76,7 +79,10 @@ parent: 6ccc786b-17f1-4562-958f-5a7d9a5a15fd - sled: bba6ea73-6c9c-4ab5-8bb4-1dd145071407 (active, config generation 2) + sled: bba6ea73-6c9c-4ab5-8bb4-1dd145071407 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -138,7 +144,10 @@ parent: 6ccc786b-17f1-4562-958f-5a7d9a5a15fd - sled: cc00b21a-5685-480a-ab5e-d2e29cf369df (active, config generation 2) + sled: cc00b21a-5685-480a-ab5e-d2e29cf369df + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ @@ -232,7 +241,10 @@ warn: no validation is done on the requested artifact hash or version blueprint cca24b71-09b5-4042-9185-b33e9f2ebba0 parent: ad97e762-7bf1-45a6-a98f-60afb7e491c0 - sled: bb0ec23a-f97c-4b6a-a5bc-864b1ebc9236 (active, config generation 2) + sled: bb0ec23a-f97c-4b6a-a5bc-864b1ebc9236 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -297,7 +309,10 @@ parent: ad97e762-7bf1-45a6-a98f-60afb7e491c0 - sled: bba6ea73-6c9c-4ab5-8bb4-1dd145071407 (active, config generation 2) + sled: bba6ea73-6c9c-4ab5-8bb4-1dd145071407 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -359,7 +374,10 @@ parent: ad97e762-7bf1-45a6-a98f-60afb7e491c0 - sled: cc00b21a-5685-480a-ab5e-d2e29cf369df (active, config generation 2) + sled: cc00b21a-5685-480a-ab5e-d2e29cf369df + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ @@ -535,7 +553,10 @@ warn: no validation is done on the requested artifact hash or version blueprint 5bf974f3-81f9-455b-b24e-3099f765664c parent: cca24b71-09b5-4042-9185-b33e9f2ebba0 - sled: bb0ec23a-f97c-4b6a-a5bc-864b1ebc9236 (active, config generation 2) + sled: bb0ec23a-f97c-4b6a-a5bc-864b1ebc9236 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -600,7 +621,10 @@ parent: cca24b71-09b5-4042-9185-b33e9f2ebba0 - sled: bba6ea73-6c9c-4ab5-8bb4-1dd145071407 (active, config generation 2) + sled: bba6ea73-6c9c-4ab5-8bb4-1dd145071407 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -662,7 +686,10 @@ parent: cca24b71-09b5-4042-9185-b33e9f2ebba0 - sled: cc00b21a-5685-480a-ab5e-d2e29cf369df (active, config generation 2) + sled: cc00b21a-5685-480a-ab5e-d2e29cf369df + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ @@ -841,7 +868,10 @@ warn: no validation is done on the requested artifact hash or version blueprint 1b837a27-3be1-4fcb-8499-a921c839e1d0 parent: 5bf974f3-81f9-455b-b24e-3099f765664c - sled: bb0ec23a-f97c-4b6a-a5bc-864b1ebc9236 (active, config generation 2) + sled: bb0ec23a-f97c-4b6a-a5bc-864b1ebc9236 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -906,7 +936,10 @@ parent: 5bf974f3-81f9-455b-b24e-3099f765664c - sled: bba6ea73-6c9c-4ab5-8bb4-1dd145071407 (active, config generation 2) + sled: bba6ea73-6c9c-4ab5-8bb4-1dd145071407 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -968,7 +1001,10 @@ parent: 5bf974f3-81f9-455b-b24e-3099f765664c - sled: cc00b21a-5685-480a-ab5e-d2e29cf369df (active, config generation 2) + sled: cc00b21a-5685-480a-ab5e-d2e29cf369df + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ @@ -1106,7 +1142,10 @@ blueprint 3682a71b-c6ca-4b7e-8f84-16df80c85960 created from blueprint 1b837a27-3 blueprint 3682a71b-c6ca-4b7e-8f84-16df80c85960 parent: 1b837a27-3be1-4fcb-8499-a921c839e1d0 - sled: bb0ec23a-f97c-4b6a-a5bc-864b1ebc9236 (active, config generation 2) + sled: bb0ec23a-f97c-4b6a-a5bc-864b1ebc9236 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -1171,7 +1210,10 @@ parent: 1b837a27-3be1-4fcb-8499-a921c839e1d0 - sled: bba6ea73-6c9c-4ab5-8bb4-1dd145071407 (active, config generation 2) + sled: bba6ea73-6c9c-4ab5-8bb4-1dd145071407 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -1233,7 +1275,10 @@ parent: 1b837a27-3be1-4fcb-8499-a921c839e1d0 - sled: cc00b21a-5685-480a-ab5e-d2e29cf369df (active, config generation 2) + sled: cc00b21a-5685-480a-ab5e-d2e29cf369df + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-set-remove-mupdate-override-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-set-remove-mupdate-override-stdout index 5f2ef7bbc74..38e000249d0 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-set-remove-mupdate-override-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-set-remove-mupdate-override-stdout @@ -49,7 +49,10 @@ blueprint 7f976e0d-d2a5-4eeb-9e82-c82bc2824aba created from latest blueprint (df blueprint 7f976e0d-d2a5-4eeb-9e82-c82bc2824aba parent: df06bb57-ad42-4431-9206-abff322896c7 - sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c (active, config generation 2) + sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -100,7 +103,10 @@ parent: df06bb57-ad42-4431-9206-abff322896c7 - sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (active, config generation 2) + sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -148,7 +154,10 @@ parent: df06bb57-ad42-4431-9206-abff322896c7 - sled: 9a867dc9-d505-427f-9eff-cdb1d4d9bd73 (active, config generation 3) + sled: 9a867dc9-d505-427f-9eff-cdb1d4d9bd73 + state:::::::::::::::::::::::: active + config generation:::::::::::: 3 + subnet::::::::::::::::::::::: fd00:1122:3344:106::/64 will remove mupdate override: 00000000-0000-0000-0000-000000000000 host phase 2 contents: @@ -197,7 +206,10 @@ parent: df06bb57-ad42-4431-9206-abff322896c7 - sled: aff6c093-197d-42c5-ad80-9f10ba051a34 (active, config generation 3) + sled: aff6c093-197d-42c5-ad80-9f10ba051a34 + state:::::::::::::::::::::::: active + config generation:::::::::::: 3 + subnet::::::::::::::::::::::: fd00:1122:3344:104::/64 will remove mupdate override: 00000000-0000-0000-0000-000000000000 host phase 2 contents: @@ -236,7 +248,10 @@ parent: df06bb57-ad42-4431-9206-abff322896c7 - sled: b82ede02-399c-48c6-a1de-411df4fa49a7 (active, config generation 3) + sled: b82ede02-399c-48c6-a1de-411df4fa49a7 + state:::::::::::::::::::::::: active + config generation:::::::::::: 3 + subnet::::::::::::::::::::::: fd00:1122:3344:105::/64 will remove mupdate override: 00000000-0000-0000-0000-000000000000 host phase 2 contents: @@ -275,7 +290,10 @@ parent: df06bb57-ad42-4431-9206-abff322896c7 - sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 (active, config generation 3) + sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 + state:::::::::::::::::::::::: active + config generation:::::::::::: 3 + subnet::::::::::::::::::::::: fd00:1122:3344:103::/64 will remove mupdate override: 00000000-0000-0000-0000-000000000000 host phase 2 contents: @@ -314,7 +332,10 @@ parent: df06bb57-ad42-4431-9206-abff322896c7 - sled: e96e226f-4ed9-4c01-91b9-69a9cd076c9e (active, config generation 3) + sled: e96e226f-4ed9-4c01-91b9-69a9cd076c9e + state:::::::::::::::::::::::: active + config generation:::::::::::: 3 + subnet::::::::::::::::::::::: fd00:1122:3344:107::/64 will remove mupdate override: 00000000-0000-0000-0000-000000000000 host phase 2 contents: @@ -695,7 +716,10 @@ blueprint ce365dff-2cdb-4f35-a186-b15e20e1e700 created from latest blueprint (af blueprint ce365dff-2cdb-4f35-a186-b15e20e1e700 parent: afb09faf-a586-4483-9289-04d4f1d8ba23 - sled: 00320471-945d-413c-85e7-03e091a70b3c (active, config generation 2) + sled: 00320471-945d-413c-85e7-03e091a70b3c + state:::::::::::::::::::::::: active + config generation:::::::::::: 2 + subnet::::::::::::::::::::::: fd00:1122:3344:108::/64 will remove mupdate override: ffffffff-ffff-ffff-ffff-ffffffffffff host phase 2 contents: @@ -725,7 +749,10 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 - sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c (active, config generation 3) + sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c + state:::::::::::::::::::::::: active + config generation:::::::::::: 3 + subnet::::::::::::::::::::::: fd00:1122:3344:102::/64 will remove mupdate override: ffffffff-ffff-ffff-ffff-ffffffffffff host phase 2 contents: @@ -777,7 +804,10 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 - sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (active, config generation 2) + sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -825,7 +855,10 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 - sled: 9a867dc9-d505-427f-9eff-cdb1d4d9bd73 (active, config generation 4) + sled: 9a867dc9-d505-427f-9eff-cdb1d4d9bd73 + state:::::::::::::::::::::::: active + config generation:::::::::::: 4 + subnet::::::::::::::::::::::: fd00:1122:3344:106::/64 will remove mupdate override: 00000000-0000-0000-0000-000000000000 host phase 2 contents: @@ -874,7 +907,10 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 - sled: aff6c093-197d-42c5-ad80-9f10ba051a34 (active, config generation 3) + sled: aff6c093-197d-42c5-ad80-9f10ba051a34 + state:::::::::::::::::::::::: active + config generation:::::::::::: 3 + subnet::::::::::::::::::::::: fd00:1122:3344:104::/64 will remove mupdate override: 00000000-0000-0000-0000-000000000000 host phase 2 contents: @@ -913,7 +949,10 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 - sled: b82ede02-399c-48c6-a1de-411df4fa49a7 (active, config generation 4) + sled: b82ede02-399c-48c6-a1de-411df4fa49a7 + state:::::::::::::::::::::::: active + config generation:::::::::::: 4 + subnet::::::::::::::::::::::: fd00:1122:3344:105::/64 will remove mupdate override: ffffffff-ffff-ffff-ffff-ffffffffffff host phase 2 contents: @@ -952,7 +991,10 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 - sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 (active, config generation 4) + sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 + state::::::::::::: active + config generation: 4 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-set-zone-images-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-set-zone-images-stdout index 388b338b306..86d9ef94924 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-set-zone-images-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-set-zone-images-stdout @@ -11,7 +11,10 @@ loaded example system with: blueprint 971eeb12-1830-4fa0-a699-98ea0164505c parent: 1b013011-2062-4b48-b544-a32b23bce83a - sled: 868d5b02-7792-4fc0-b6a9-654afcae9ea0 (active, config generation 2) + sled: 868d5b02-7792-4fc0-b6a9-654afcae9ea0 + state::::::::::::: active + config generation: 2 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -133,7 +136,10 @@ warn: no validation is done on the requested image source blueprint f714e6ea-e85a-4d7d-93c2-a018744fe176 parent: 9766ca20-38d4-4380-b005-e7c43c797e7c - sled: 868d5b02-7792-4fc0-b6a9-654afcae9ea0 (active, config generation 4) + sled: 868d5b02-7792-4fc0-b6a9-654afcae9ea0 + state::::::::::::: active + config generation: 4 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -371,7 +377,10 @@ warn: no validation is done on the requested image source blueprint d9c572a1-a68c-4945-b1ec-5389bd588fe9 parent: bb128f06-a2e1-44c1-8874-4f789d0ff896 - sled: 868d5b02-7792-4fc0-b6a9-654afcae9ea0 (active, config generation 6) + sled: 868d5b02-7792-4fc0-b6a9-654afcae9ea0 + state::::::::::::: active + config generation: 6 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-target-release-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-target-release-stdout index 801845e6a3b..66ce6ecbbeb 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-target-release-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-target-release-stdout @@ -9050,7 +9050,10 @@ external DNS: blueprint 008e1541-3d9d-4a50-a877-eed4a3cf86ab parent: 05685571-d61f-4754-a2b2-604ea8c45dff - sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c (active, config generation 18) + sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c + state::::::::::::: active + config generation: 18 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------------ @@ -9127,7 +9130,10 @@ parent: 05685571-d61f-4754-a2b2-604ea8c45dff - sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (active, config generation 19) + sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 + state::::::::::::: active + config generation: 19 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------------ @@ -9203,7 +9209,10 @@ parent: 05685571-d61f-4754-a2b2-604ea8c45dff - sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 (active, config generation 17) + sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 + state::::::::::::: active + config generation: 17 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------------ diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-unsafe-zone-mgs-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-unsafe-zone-mgs-stdout index 7492024e0bc..691209d2742 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-unsafe-zone-mgs-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-unsafe-zone-mgs-stdout @@ -1079,7 +1079,10 @@ d81c6a84-79b8-4958-ae41-ea46c9b19763 serial2 3 fd00:1122:3344:103::/64 blueprint 58d5e830-0884-47d8-a7cd-b2b3751adeb4 parent: 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 - sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c (active, config generation 3) + sled: 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c + state::::::::::::: active + config generation: 3 + subnet:::::::::::: fd00:1122:3344:102::/64 host phase 2 contents: ------------------------ @@ -1144,7 +1147,10 @@ parent: 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 - sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (active, config generation 3) + sled: 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 + state::::::::::::: active + config generation: 3 + subnet:::::::::::: fd00:1122:3344:101::/64 host phase 2 contents: ------------------------ @@ -1206,7 +1212,10 @@ parent: 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 - sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 (active, config generation 3) + sled: d81c6a84-79b8-4958-ae41-ea46c9b19763 + state::::::::::::: active + config generation: 3 + subnet:::::::::::: fd00:1122:3344:103::/64 host phase 2 contents: ------------------------ From 831d72818d34a2d7bcc6229ae91da0756526f1b3 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 17 Nov 2025 13:23:49 -0500 Subject: [PATCH 19/24] update RSS to include sled subnet in blueprint --- sled-agent/src/rack_setup/plan/service.rs | 3 +++ sled-agent/src/sim/server.rs | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/sled-agent/src/rack_setup/plan/service.rs b/sled-agent/src/rack_setup/plan/service.rs index 34c43da981d..869c967ed18 100644 --- a/sled-agent/src/rack_setup/plan/service.rs +++ b/sled-agent/src/rack_setup/plan/service.rs @@ -171,6 +171,7 @@ impl SledConfig { pub(crate) struct PlannedSledDescription { pub(crate) underlay_address: SocketAddrV6, pub(crate) sled_id: SledUuid, + pub(crate) subnet: Ipv6Subnet, pub(crate) config: SledConfig, } @@ -812,6 +813,7 @@ impl Plan { .map(|sled_info| PlannedSledDescription { underlay_address: sled_info.sled_address, sled_id: sled_info.sled_id, + subnet: sled_info.subnet, config: sled_info.request, }) .collect(); @@ -910,6 +912,7 @@ impl Plan { sled_description.sled_id, BlueprintSledConfig { state: SledState::Active, + subnet: sled_description.subnet, sled_agent_generation: sled_agent_config_generation, disks: sled_config.disks.clone(), datasets, diff --git a/sled-agent/src/sim/server.rs b/sled-agent/src/sim/server.rs index df511660761..a00f2f38202 100644 --- a/sled-agent/src/sim/server.rs +++ b/sled-agent/src/sim/server.rs @@ -40,8 +40,8 @@ use nexus_types::deployment::{ }; use nexus_types::inventory::NetworkInterfaceKind; use omicron_common::FileKv; -use omicron_common::address::DNS_OPTE_IPV4_SUBNET; use omicron_common::address::NEXUS_OPTE_IPV4_SUBNET; +use omicron_common::address::{DNS_OPTE_IPV4_SUBNET, Ipv6Subnet}; use omicron_common::api::external::Generation; use omicron_common::api::external::MacAddr; use omicron_common::api::external::Vni; @@ -590,6 +590,7 @@ pub async fn run_standalone_server( all_sleds.insert_overwrite(PlannedSledDescription { underlay_address, sled_id: config.id, + subnet: Ipv6Subnet::new(*underlay_address.ip()), config: SledConfig { disks: omicron_physical_disks_config .disks From e43a038d85c076567172febb04da6fac43d3a034 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 17 Nov 2025 13:48:41 -0500 Subject: [PATCH 20/24] add subnet column to bp_sled_metadata table --- common/src/address.rs | 8 ++++++++ nexus/db-model/src/deployment.rs | 17 +++++++++++++++++ nexus/db-queries/src/db/datastore/deployment.rs | 8 ++++++++ nexus/db-schema/src/schema.rs | 2 ++ nexus/test-utils/src/lib.rs | 2 ++ schema/crdb/dbinit.sql | 3 +++ 6 files changed, 40 insertions(+) diff --git a/common/src/address.rs b/common/src/address.rs index 20e5f8bbca3..4a8b19d1f2f 100644 --- a/common/src/address.rs +++ b/common/src/address.rs @@ -310,6 +310,14 @@ impl From for Ipv6Subnet { } } +impl From> for Ipv6Network { + fn from(net: Ipv6Subnet) -> Self { + // Ipv6Subnet::new() asserts that `N` is a valid IPv6 prefix, so it's + // okay to unwrap here. + Self::new(net.net.prefix(), N).unwrap() + } +} + // We need a custom Deserialize to ensure that the subnet is what we expect. impl<'de, const N: u8> Deserialize<'de> for Ipv6Subnet { fn deserialize(deserializer: D) -> Result diff --git a/nexus/db-model/src/deployment.rs b/nexus/db-model/src/deployment.rs index 37b1a1de795..02c6d3b5404 100644 --- a/nexus/db-model/src/deployment.rs +++ b/nexus/db-model/src/deployment.rs @@ -58,6 +58,8 @@ use nexus_types::deployment::{ OmicronZoneExternalSnatIp, }; use nexus_types::inventory::BaseboardId; +use omicron_common::address::Ipv6Subnet; +use omicron_common::address::SLED_PREFIX; use omicron_common::api::internal::shared::NetworkInterface; use omicron_common::disk::DiskIdentity; use omicron_common::zpool_name::ZpoolName; @@ -217,9 +219,24 @@ pub struct BpSledMetadata { pub remove_mupdate_override: Option>, pub host_phase_2_desired_slot_a: Option, pub host_phase_2_desired_slot_b: Option, + /// Public only for easy of writing queries; consumers should prefer the + /// `subnet()` method. + pub subnet: IpNetwork, } impl BpSledMetadata { + pub fn subnet(&self) -> anyhow::Result> { + let subnet = match self.subnet { + IpNetwork::V4(subnet) => bail!( + "invalid subnet for sled {}: {subnet} (should be Ipv6)", + self.sled_id + ), + IpNetwork::V6(subnet) => subnet, + }; + + Ok(subnet.into()) + } + pub fn host_phase_2( &self, slot_a_artifact_version: Option, diff --git a/nexus/db-queries/src/db/datastore/deployment.rs b/nexus/db-queries/src/db/datastore/deployment.rs index ffb44a69c7a..7c8f783aec9 100644 --- a/nexus/db-queries/src/db/datastore/deployment.rs +++ b/nexus/db-queries/src/db/datastore/deployment.rs @@ -17,6 +17,7 @@ use async_bb8_diesel::AsyncSimpleConnection; use chrono::DateTime; use chrono::Utc; use clickhouse_admin_types::{KeeperId, ServerId}; +use ipnetwork::Ipv6Network; use core::future::Future; use core::pin::Pin; use diesel::BoolExpressionMethods; @@ -262,6 +263,7 @@ impl DataStore { .slot_b .artifact_hash() .map(ArtifactHash), + subnet: Ipv6Network::from(sled.subnet).into(), }) .collect::>(); @@ -793,8 +795,14 @@ impl DataStore { paginator = p.found_batch(&batch, &|(s, _, _)| s.sled_id); for (s, slot_a_version, slot_b_version) in batch { + let subnet = s.subnet().map_err(|e| { + Error::internal_error( + &InlineErrorChain::new(&*e).to_string(), + ) + })?; let config = BlueprintSledConfig { state: s.sled_state.into(), + subnet, sled_agent_generation: *s.sled_agent_generation, disks: IdOrdMap::new(), datasets: IdOrdMap::new(), diff --git a/nexus/db-schema/src/schema.rs b/nexus/db-schema/src/schema.rs index 3f8576b0d14..29cca5f5daa 100644 --- a/nexus/db-schema/src/schema.rs +++ b/nexus/db-schema/src/schema.rs @@ -2044,6 +2044,8 @@ table! { host_phase_2_desired_slot_a -> Nullable, host_phase_2_desired_slot_b -> Nullable, + + subnet -> Inet, } } diff --git a/nexus/test-utils/src/lib.rs b/nexus/test-utils/src/lib.rs index 2b801ba6694..79f52a213f6 100644 --- a/nexus/test-utils/src/lib.rs +++ b/nexus/test-utils/src/lib.rs @@ -65,6 +65,7 @@ use nexus_types::deployment::ReconfiguratorConfig; use nexus_types::deployment::blueprint_zone_type; use nexus_types::external_api::views::SledState; use nexus_types::internal_api::params::DnsConfigParams; +use omicron_common::address::Ipv6Subnet; use omicron_common::address::DNS_OPTE_IPV4_SUBNET; use omicron_common::address::NEXUS_OPTE_IPV4_SUBNET; use omicron_common::address::NTP_OPTE_IPV4_SUBNET; @@ -1708,6 +1709,7 @@ impl<'a, N: NexusServer> ControlPlaneTestContextBuilder<'a, N> { sled_id, BlueprintSledConfig { state: SledState::Active, + subnet: Ipv6Subnet::new(Ipv6Addr::LOCALHOST), sled_agent_generation, disks, datasets, diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index 9ce28021a5f..b65e80a8d7b 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -4791,6 +4791,9 @@ CREATE TABLE IF NOT EXISTS omicron.public.bp_sled_metadata ( host_phase_2_desired_slot_a STRING(64), host_phase_2_desired_slot_b STRING(64), + -- the sled's /64 subnet on the underlay address + subnet INET NOT NULL, + PRIMARY KEY (blueprint_id, sled_id) ); From 9d71e864e04a281a8fd6d495b939927ffa63c11b Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 17 Nov 2025 15:06:07 -0500 Subject: [PATCH 21/24] db migration --- nexus/db-model/src/schema_versions.rs | 3 +- .../background/tasks/blueprint_execution.rs | 4 +- nexus/tests/integration_tests/schema.rs | 125 ++++++++++++++++++ .../crdb/blueprint-sled-config-subnet/up1.sql | 2 + .../crdb/blueprint-sled-config-subnet/up2.sql | 8 ++ .../crdb/blueprint-sled-config-subnet/up3.sql | 2 + schema/crdb/dbinit.sql | 2 +- 7 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 schema/crdb/blueprint-sled-config-subnet/up1.sql create mode 100644 schema/crdb/blueprint-sled-config-subnet/up2.sql create mode 100644 schema/crdb/blueprint-sled-config-subnet/up3.sql diff --git a/nexus/db-model/src/schema_versions.rs b/nexus/db-model/src/schema_versions.rs index 1ee9a2a936a..691c8a5ecd6 100644 --- a/nexus/db-model/src/schema_versions.rs +++ b/nexus/db-model/src/schema_versions.rs @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock}; /// /// This must be updated when you change the database schema. Refer to /// schema/crdb/README.adoc in the root of this repository for details. -pub const SCHEMA_VERSION: Version = Version::new(209, 0, 0); +pub const SCHEMA_VERSION: Version = Version::new(210, 0, 0); /// List of all past database schema versions, in *reverse* order /// @@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock> = LazyLock::new(|| { // | leaving the first copy as an example for the next person. // v // KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"), + KnownVersion::new(210, "blueprint-sled-config-subnet"), KnownVersion::new(209, "multicast-group-support"), KnownVersion::new(208, "disable-tuf-repo-pruner"), KnownVersion::new(207, "disk-types"), diff --git a/nexus/src/app/background/tasks/blueprint_execution.rs b/nexus/src/app/background/tasks/blueprint_execution.rs index 53a86896c82..0e3d7b64dab 100644 --- a/nexus/src/app/background/tasks/blueprint_execution.rs +++ b/nexus/src/app/background/tasks/blueprint_execution.rs @@ -250,17 +250,18 @@ mod test { blueprint_zone_type, }; use nexus_types::external_api::views::SledState; + use omicron_common::address::Ipv6Subnet; use omicron_common::api::external; use omicron_common::api::external::Generation; use omicron_common::zpool_name::ZpoolName; use omicron_uuid_kinds::BlueprintUuid; - use omicron_uuid_kinds::OmicronZoneUuid; use omicron_uuid_kinds::PhysicalDiskUuid; use omicron_uuid_kinds::SledUuid; use omicron_uuid_kinds::ZpoolUuid; use serde_json::json; use std::collections::BTreeMap; + use std::net::Ipv6Addr; use std::net::SocketAddr; use std::sync::Arc; use tokio::sync::watch; @@ -285,6 +286,7 @@ mod test { sled_id, BlueprintSledConfig { state: SledState::Active, + subnet: Ipv6Subnet::new(Ipv6Addr::LOCALHOST), sled_agent_generation: Generation::new().next(), disks: IdOrdMap::new(), datasets: IdOrdMap::new(), diff --git a/nexus/tests/integration_tests/schema.rs b/nexus/tests/integration_tests/schema.rs index 11cc874287a..7f17d4fb44a 100644 --- a/nexus/tests/integration_tests/schema.rs +++ b/nexus/tests/integration_tests/schema.rs @@ -3476,6 +3476,125 @@ fn after_207_0_0<'a>(ctx: &'a MigrationContext<'a>) -> BoxFuture<'a, ()> { }) } +mod migration_210 { + use super::*; + use omicron_common::address::Ipv6Subnet; + use omicron_common::address::SLED_PREFIX; + use pretty_assertions::assert_eq; + use std::collections::BTreeSet; + + // randomly-generated IDs + const SLED_ID_1: &str = "48e1d10b-3ec8-4abd-95e2-5f37f79da546"; + const SLED_ID_2: &str = "2144219e-d989-4929-b0b3-db895f51b7b5"; + const SLED_ID_3: &str = "d2de378b-c788-4940-9a11-60ac4e633f0c"; + + // randomly-generated IPs + const SLED_IP_1: &str = "445c:212f:f2bc:f202:2b90:5b0e:2d6c:585e"; + const SLED_IP_2: &str = "05fb:7e45:22b1:8dfb:9a06:0dbf:22ab:3eb4"; + const SLED_IP_3: &str = "92a6:42c8:0cf2:7556:5ace:5bab:26e4:4dd1"; + + async fn before_impl(ctx: &MigrationContext<'_>) { + ctx.client + .batch_execute(&format!( + " + INSERT INTO omicron.public.sled ( + id, time_created, time_modified, rcgen, rack_id, is_scrimlet, + serial_number, part_number, revision, usable_hardware_threads, + usable_physical_ram, reservoir_size, ip, port, last_used_address, + sled_policy, sled_state, repo_depot_port, cpu_family + ) + VALUES + ('{SLED_ID_1}', now(), now(), 1, gen_random_uuid(), false, + 'migration-210-serial1', 'part1', 1, 64, 12345678, 123456, + '{SLED_IP_1}', 12345, '{SLED_IP_1}', 'in_service', 'active', + 12346, 'unknown'), + ('{SLED_ID_2}', now(), now(), 1, gen_random_uuid(), false, + 'migration-210-serial2', 'part1', 1, 64, 12345678, 123456, + '{SLED_IP_2}', 12345, '{SLED_IP_2}', 'no_provision', 'active', + 12346, 'unknown'), + ('{SLED_ID_3}', now(), now(), 1, gen_random_uuid(), false, + 'migration-210-serial3', 'part1', 1, 64, 12345678, 123456, + '{SLED_IP_3}', 12345, '{SLED_IP_3}', 'expunged', 'decommissioned', + 12346, 'unknown'); + + INSERT INTO omicron.public.bp_sled_metadata ( + blueprint_id, sled_id, sled_state, sled_agent_generation, + remove_mupdate_override, host_phase_2_desired_slot_a, + host_phase_2_desired_slot_b + ) + VALUES + (gen_random_uuid(), '{SLED_ID_1}', 'active', 1, NULL, NULL, NULL), + (gen_random_uuid(), '{SLED_ID_2}', 'active', 1, NULL, NULL, NULL), + (gen_random_uuid(), '{SLED_ID_3}', 'decommissioned', 1, + NULL, NULL, NULL); + " + )) + .await + .expect("inserted pre-migration data"); + } + + async fn after_impl(ctx: &MigrationContext<'_>) { + let sled_id_1: Uuid = SLED_ID_1.parse().unwrap(); + let sled_id_2: Uuid = SLED_ID_2.parse().unwrap(); + let sled_id_3: Uuid = SLED_ID_3.parse().unwrap(); + + let expected = [ + ( + sled_id_1, + Ipv6Subnet::::new(SLED_IP_1.parse().unwrap()) + .to_string(), + ), + ( + sled_id_2, + Ipv6Subnet::::new(SLED_IP_2.parse().unwrap()) + .to_string(), + ), + ( + sled_id_3, + Ipv6Subnet::::new(SLED_IP_3.parse().unwrap()) + .to_string(), + ), + ] + .into_iter() + .collect::>(); + + let rows = ctx + .client + .query( + " + SELECT sled_id, subnet::text + FROM omicron.public.bp_sled_metadata + WHERE sled_id IN ($1, $2, $3) + ", + &[&sled_id_1, &sled_id_2, &sled_id_3], + ) + .await + .expect("queried post-migration data"); + let got = rows + .into_iter() + .map(|row| { + ( + row.get::<_, Uuid>("sled_id"), + row.get::<_, String>("subnet"), + ) + }) + .collect::>(); + assert_eq!(expected, got); + } + + pub(super) fn before<'a>( + ctx: &'a MigrationContext<'a>, + ) -> BoxFuture<'a, ()> { + Box::pin(before_impl(ctx)) + } + + pub(super) fn after<'a>( + ctx: &'a MigrationContext<'a>, + ) -> BoxFuture<'a, ()> { + Box::pin(after_impl(ctx)) + } +} + // Lazily initializes all migration checks. The combination of Rust function // pointers and async makes defining a static table fairly painful, so we're // using lazy initialization instead. @@ -3586,6 +3705,12 @@ fn get_migration_checks() -> BTreeMap { Version::new(207, 0, 0), DataMigrationFns::new().before(before_207_0_0).after(after_207_0_0), ); + map.insert( + Version::new(210, 0, 0), + DataMigrationFns::new() + .before(migration_210::before) + .after(migration_210::after), + ); map } diff --git a/schema/crdb/blueprint-sled-config-subnet/up1.sql b/schema/crdb/blueprint-sled-config-subnet/up1.sql new file mode 100644 index 00000000000..0d619ea8eea --- /dev/null +++ b/schema/crdb/blueprint-sled-config-subnet/up1.sql @@ -0,0 +1,2 @@ +ALTER TABLE omicron.public.bp_sled_metadata + ADD COLUMN IF NOT EXISTS subnet INET; diff --git a/schema/crdb/blueprint-sled-config-subnet/up2.sql b/schema/crdb/blueprint-sled-config-subnet/up2.sql new file mode 100644 index 00000000000..3d33f7b2619 --- /dev/null +++ b/schema/crdb/blueprint-sled-config-subnet/up2.sql @@ -0,0 +1,8 @@ +SET LOCAL disallow_full_table_scans = off; + +UPDATE omicron.public.bp_sled_metadata as bp + SET subnet = ( + SELECT set_masklen(netmask('::/64') & ip, 64) + FROM omicron.public.sled AS s + WHERE bp.sled_id = s.id + ); diff --git a/schema/crdb/blueprint-sled-config-subnet/up3.sql b/schema/crdb/blueprint-sled-config-subnet/up3.sql new file mode 100644 index 00000000000..3747d8d38cf --- /dev/null +++ b/schema/crdb/blueprint-sled-config-subnet/up3.sql @@ -0,0 +1,2 @@ +ALTER TABLE omicron.public.bp_sled_metadata + ALTER COLUMN subnet SET NOT NULL; diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index b65e80a8d7b..2dd10ed5973 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -7381,7 +7381,7 @@ INSERT INTO omicron.public.db_metadata ( version, target_version ) VALUES - (TRUE, NOW(), NOW(), '209.0.0', NULL) + (TRUE, NOW(), NOW(), '210.0.0', NULL) ON CONFLICT DO NOTHING; COMMIT; From ef51fa228309493ee64f49d7d251049695c02202 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 17 Nov 2025 16:44:43 -0500 Subject: [PATCH 22/24] minor test fixes --- dev-tools/omdb/tests/successes.out | 20 ++++- .../reconfigurator/execution/src/database.rs | 74 +++++++++++-------- nexus/reconfigurator/execution/src/dns.rs | 1 + .../execution/src/omicron_sled_config.rs | 3 + 4 files changed, 63 insertions(+), 35 deletions(-) diff --git a/dev-tools/omdb/tests/successes.out b/dev-tools/omdb/tests/successes.out index 0d3c4203166..b6c37e6f52e 100644 --- a/dev-tools/omdb/tests/successes.out +++ b/dev-tools/omdb/tests/successes.out @@ -1552,7 +1552,10 @@ stdout: blueprint ............. parent: - sled: ..................... (active, config generation 2) + sled: ..................... + state::::::::::::: active + config generation: 2 + subnet:::::::::::: ::/64 host phase 2 contents: ------------------------ @@ -1591,7 +1594,10 @@ parent: - sled: ..................... (active, config generation 2) + sled: ..................... + state::::::::::::: active + config generation: 2 + subnet:::::::::::: ::/64 host phase 2 contents: ------------------------ @@ -1676,7 +1682,10 @@ stdout: blueprint ............. parent: - sled: ..................... (active, config generation 2) + sled: ..................... + state::::::::::::: active + config generation: 2 + subnet:::::::::::: ::/64 host phase 2 contents: ------------------------ @@ -1715,7 +1724,10 @@ parent: - sled: ..................... (active, config generation 2) + sled: ..................... + state::::::::::::: active + config generation: 2 + subnet:::::::::::: ::/64 host phase 2 contents: ------------------------ diff --git a/nexus/reconfigurator/execution/src/database.rs b/nexus/reconfigurator/execution/src/database.rs index 075ecb7af33..5a3b691de81 100644 --- a/nexus/reconfigurator/execution/src/database.rs +++ b/nexus/reconfigurator/execution/src/database.rs @@ -79,12 +79,14 @@ mod test { use nexus_types::deployment::BlueprintZoneImageSource; use nexus_types::deployment::BlueprintZoneType; use nexus_types::deployment::CockroachDbPreserveDowngrade; + use nexus_types::deployment::OmicronZoneExternalFloatingIp; use nexus_types::deployment::OximeterReadMode; use nexus_types::deployment::PendingMgsUpdates; use nexus_types::deployment::blueprint_zone_type; use nexus_types::external_api::views::SledState; use nexus_types::inventory::NetworkInterface; use nexus_types::inventory::NetworkInterfaceKind; + use omicron_common::address::Ipv6Subnet; use omicron_common::api::external::Error; use omicron_common::api::external::Generation; use omicron_common::api::external::MacAddr; @@ -98,6 +100,8 @@ mod test { use omicron_uuid_kinds::SledUuid; use omicron_uuid_kinds::ZpoolUuid; use std::collections::BTreeMap; + use std::net::IpAddr; + use std::net::Ipv6Addr; fn create_test_blueprint( top_level_nexus_generation: Generation, @@ -112,38 +116,45 @@ mod test { let zones: IdOrdMap = nexus_zones .into_iter() - .map(|(zone_id, disposition, nexus_generation)| BlueprintZoneConfig { - disposition, - id: zone_id, - filesystem_pool: ZpoolName::new_external(ZpoolUuid::new_v4()), - zone_type: BlueprintZoneType::Nexus(blueprint_zone_type::Nexus { - internal_address: "[::1]:0".parse().unwrap(), - lockstep_port: 0, - external_dns_servers: Vec::new(), - external_ip: nexus_types::deployment::OmicronZoneExternalFloatingIp { - id: ExternalIpUuid::new_v4(), - ip: std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST), - }, - external_tls: true, - nic: NetworkInterface { - id: uuid::Uuid::new_v4(), - kind: NetworkInterfaceKind::Service { - id: zone_id.into_untyped_uuid(), + .map(|(zone_id, disposition, nexus_generation)| { + BlueprintZoneConfig { + disposition, + id: zone_id, + filesystem_pool: ZpoolName::new_external( + ZpoolUuid::new_v4(), + ), + zone_type: BlueprintZoneType::Nexus( + blueprint_zone_type::Nexus { + internal_address: "[::1]:0".parse().unwrap(), + lockstep_port: 0, + external_dns_servers: Vec::new(), + external_ip: OmicronZoneExternalFloatingIp { + id: ExternalIpUuid::new_v4(), + ip: IpAddr::V6(Ipv6Addr::LOCALHOST), + }, + external_tls: true, + nic: NetworkInterface { + id: uuid::Uuid::new_v4(), + kind: NetworkInterfaceKind::Service { + id: zone_id.into_untyped_uuid(), + }, + name: "test-nic".parse().unwrap(), + ip: "192.168.1.1".parse().unwrap(), + mac: MacAddr::random_system(), + subnet: ipnetwork::IpNetwork::V4( + "192.168.1.0/24".parse().unwrap(), + ) + .into(), + vni: Vni::try_from(100).unwrap(), + primary: true, + slot: 0, + transit_ips: Vec::new(), + }, + nexus_generation, }, - name: "test-nic".parse().unwrap(), - ip: "192.168.1.1".parse().unwrap(), - mac: MacAddr::random_system(), - subnet: ipnetwork::IpNetwork::V4( - "192.168.1.0/24".parse().unwrap() - ).into(), - vni: Vni::try_from(100).unwrap(), - primary: true, - slot: 0, - transit_ips: Vec::new(), - }, - nexus_generation, - }), - image_source: BlueprintZoneImageSource::InstallDataset, + ), + image_source: BlueprintZoneImageSource::InstallDataset, + } }) .collect(); @@ -152,6 +163,7 @@ mod test { sled_id, BlueprintSledConfig { state: SledState::Active, + subnet: Ipv6Subnet::new(Ipv6Addr::LOCALHOST), sled_agent_generation: Generation::new(), zones, disks: IdOrdMap::new(), diff --git a/nexus/reconfigurator/execution/src/dns.rs b/nexus/reconfigurator/execution/src/dns.rs index fb932f75792..45813489d7f 100644 --- a/nexus/reconfigurator/execution/src/dns.rs +++ b/nexus/reconfigurator/execution/src/dns.rs @@ -706,6 +706,7 @@ mod test { sa.sled_id, BlueprintSledConfig { state: SledState::Active, + subnet: Ipv6Subnet::new(*sa.sled_agent_address.ip()), sled_agent_generation: ledgered_sled_config.generation, disks: IdOrdMap::new(), datasets: IdOrdMap::new(), diff --git a/nexus/reconfigurator/execution/src/omicron_sled_config.rs b/nexus/reconfigurator/execution/src/omicron_sled_config.rs index 1d09b8708af..5a3758976d9 100644 --- a/nexus/reconfigurator/execution/src/omicron_sled_config.rs +++ b/nexus/reconfigurator/execution/src/omicron_sled_config.rs @@ -97,6 +97,7 @@ mod tests { use nexus_types::external_api::views::SledPolicy; use nexus_types::external_api::views::SledProvisionPolicy; use nexus_types::external_api::views::SledState; + use omicron_common::address::Ipv6Subnet; use omicron_common::address::REPO_DEPOT_PORT; use omicron_common::api::external::Generation; use omicron_common::api::internal::shared::DatasetKind; @@ -109,6 +110,7 @@ mod tests { use omicron_uuid_kinds::OmicronZoneUuid; use omicron_uuid_kinds::PhysicalDiskUuid; use omicron_uuid_kinds::ZpoolUuid; + use std::net::Ipv6Addr; use std::net::SocketAddr; type ControlPlaneTestContext = @@ -259,6 +261,7 @@ mod tests { let sled_config = BlueprintSledConfig { state: SledState::Active, + subnet: Ipv6Subnet::new(Ipv6Addr::LOCALHOST), sled_agent_generation: sim_sled_agent_config_generation.next(), disks, datasets, From 660056e9921e26990733a9527d4dbd5b81812fcc Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Tue, 18 Nov 2025 09:50:19 -0500 Subject: [PATCH 23/24] update lockstep openapi --- openapi/nexus-lockstep.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openapi/nexus-lockstep.json b/openapi/nexus-lockstep.json index ce07eeaac5a..3ea4798c36d 100644 --- a/openapi/nexus-lockstep.json +++ b/openapi/nexus-lockstep.json @@ -2389,6 +2389,9 @@ "state": { "$ref": "#/components/schemas/SledState" }, + "subnet": { + "$ref": "#/components/schemas/Ipv6Subnet" + }, "zones": { "title": "IdOrdMap", "x-rust-type": { @@ -2414,6 +2417,7 @@ "host_phase_2", "sled_agent_generation", "state", + "subnet", "zones" ] }, @@ -5338,6 +5342,18 @@ "last" ] }, + "Ipv6Subnet": { + "description": "Wraps an [`Ipv6Net`] with a compile-time prefix length.", + "type": "object", + "properties": { + "net": { + "$ref": "#/components/schemas/Ipv6Net" + } + }, + "required": [ + "net" + ] + }, "KeeperId": { "description": "A unique ID for a ClickHouse Keeper", "type": "integer", From aa858858aeadc79ada10116839045ed6fb61542e Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Tue, 18 Nov 2025 14:12:14 -0500 Subject: [PATCH 24/24] cargo fmt --- nexus/db-queries/src/db/datastore/deployment.rs | 2 +- nexus/test-utils/src/lib.rs | 2 +- nexus/tests/integration_tests/schema.rs | 5 +---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/nexus/db-queries/src/db/datastore/deployment.rs b/nexus/db-queries/src/db/datastore/deployment.rs index 7c8f783aec9..3b5f406cd86 100644 --- a/nexus/db-queries/src/db/datastore/deployment.rs +++ b/nexus/db-queries/src/db/datastore/deployment.rs @@ -17,7 +17,6 @@ use async_bb8_diesel::AsyncSimpleConnection; use chrono::DateTime; use chrono::Utc; use clickhouse_admin_types::{KeeperId, ServerId}; -use ipnetwork::Ipv6Network; use core::future::Future; use core::pin::Pin; use diesel::BoolExpressionMethods; @@ -42,6 +41,7 @@ use diesel::sql_types; use diesel::sql_types::Nullable; use futures::FutureExt; use iddqd::IdOrdMap; +use ipnetwork::Ipv6Network; use nexus_db_errors::ErrorHandler; use nexus_db_errors::OptionalError; use nexus_db_errors::TransactionError; diff --git a/nexus/test-utils/src/lib.rs b/nexus/test-utils/src/lib.rs index 79f52a213f6..da63e103731 100644 --- a/nexus/test-utils/src/lib.rs +++ b/nexus/test-utils/src/lib.rs @@ -65,8 +65,8 @@ use nexus_types::deployment::ReconfiguratorConfig; use nexus_types::deployment::blueprint_zone_type; use nexus_types::external_api::views::SledState; use nexus_types::internal_api::params::DnsConfigParams; -use omicron_common::address::Ipv6Subnet; use omicron_common::address::DNS_OPTE_IPV4_SUBNET; +use omicron_common::address::Ipv6Subnet; use omicron_common::address::NEXUS_OPTE_IPV4_SUBNET; use omicron_common::address::NTP_OPTE_IPV4_SUBNET; use omicron_common::address::NTP_PORT; diff --git a/nexus/tests/integration_tests/schema.rs b/nexus/tests/integration_tests/schema.rs index 7f17d4fb44a..9c00a3b8e32 100644 --- a/nexus/tests/integration_tests/schema.rs +++ b/nexus/tests/integration_tests/schema.rs @@ -3573,10 +3573,7 @@ mod migration_210 { let got = rows .into_iter() .map(|row| { - ( - row.get::<_, Uuid>("sled_id"), - row.get::<_, String>("subnet"), - ) + (row.get::<_, Uuid>("sled_id"), row.get::<_, String>("subnet")) }) .collect::>(); assert_eq!(expected, got);