Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1ede6a9
add expunged_and_unreferenced
jgallagher Dec 11, 2025
73da04c
input/builder helper methods
jgallagher Jan 13, 2026
4a4f0a0
initial collecting "expunged and unreferenced"
jgallagher Jan 14, 2026
97d3b23
flesh out oximeter reference check
jgallagher Jan 16, 2026
c2eb0e3
flesh out is_nexus_referenced()
jgallagher Jan 27, 2026
5cad9b2
docs on static_check_all_reasons_handled()
jgallagher Jan 27, 2026
1315325
cleanup and comments
jgallagher Jan 27, 2026
c80d7e6
cargo fmt
jgallagher Jan 27, 2026
9cb4c51
Merge branch 'main' into john/planning-input-pruneable
jgallagher Jan 30, 2026
70a506f
comments
jgallagher Jan 30, 2026
b5bf895
WIP: initial runtime reason coverage checking
jgallagher Jan 30, 2026
fe3d0bf
wip
jgallagher Jan 30, 2026
b0ca4f4
new caching helpers
jgallagher Jan 30, 2026
f0b31ba
reorg into PruneableZones
jgallagher Jan 30, 2026
6c51581
initial test (failing)
jgallagher Jan 30, 2026
1e3dd23
cleanup and clarify lazy sets
jgallagher Jan 30, 2026
ad69b2c
s/referenced/pruneable/ (and invert function return values)
jgallagher Jan 30, 2026
17772e6
add boundary NTP support to ExampleSystemBuilder
jgallagher Jan 30, 2026
8d0a365
flesh out test_pruneable_zones_reason_checker()
jgallagher Jan 30, 2026
b2dc8f2
add test_oximeter_pruneable_reasons()
jgallagher Jan 30, 2026
864dade
add test_boundary_ntp_pruneable_reasons()
jgallagher Jan 31, 2026
c51cd3f
initial remaining tests
jgallagher Jan 31, 2026
51298b4
flesh out Nexus test
jgallagher Feb 2, 2026
06febb9
minor cleanup (comments, enum variant name)
jgallagher Feb 2, 2026
ff208bc
module rename
jgallagher Feb 2, 2026
dcdc772
expunged_and_unreferenced -> pruneable
jgallagher Feb 2, 2026
28329f1
Merge remote-tracking branch 'origin/main' into john/planning-input-p…
jgallagher Feb 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 30 additions & 17 deletions nexus/db-queries/src/db/datastore/saga.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ use nexus_auth::context::OpContext;
use nexus_db_errors::ErrorHandler;
use nexus_db_errors::public_error_from_diesel;
use nexus_db_model::SagaState;
use omicron_common::api::external::DataPageParams;
use omicron_common::api::external::Error;
use omicron_common::api::external::LookupType;
use omicron_common::api::external::ResourceType;
use std::ops::Add;
use uuid::Uuid;

impl DataStore {
pub async fn saga_create(
Expand Down Expand Up @@ -130,6 +132,26 @@ impl DataStore {
}
}

/// Returns a single page of unfinished sagas assigned to SEC `sec_id`.
pub async fn saga_list_recovery_candidates(
&self,
opctx: &OpContext,
sec_id: db::saga_types::SecId,
pagparams: &DataPageParams<'_, Uuid>,
) -> Result<Vec<db::saga_types::Saga>, Error> {
use nexus_db_schema::schema::saga::dsl;
let conn = self.pool_connection_authorized(opctx).await?;
paginated(dsl::saga, dsl::id, pagparams)
.filter(
dsl::saga_state.eq_any(SagaState::RECOVERY_CANDIDATE_STATES),
)
.filter(dsl::current_sec.eq(sec_id))
.select(db::saga_types::Saga::as_select())
.load_async(&*conn)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
}

/// Returns a list of unfinished sagas assigned to SEC `sec_id`, making as
/// many queries as needed (in batches) to get them all
pub async fn saga_list_recovery_candidates_batched(
Expand All @@ -142,25 +164,16 @@ impl DataStore {
SQL_BATCH_SIZE,
dropshot::PaginationOrder::Ascending,
);
let conn = self.pool_connection_authorized(opctx).await?;
while let Some(p) = paginator.next() {
use nexus_db_schema::schema::saga::dsl;

let mut batch =
paginated(dsl::saga, dsl::id, &p.current_pagparams())
.filter(
dsl::saga_state
.eq_any(SagaState::RECOVERY_CANDIDATE_STATES),
)
.filter(dsl::current_sec.eq(sec_id))
.select(db::saga_types::Saga::as_select())
.load_async(&*conn)
.await
.map_err(|e| {
public_error_from_diesel(e, ErrorHandler::Server)
})?;
let mut batch = self
.saga_list_recovery_candidates(
opctx,
sec_id,
&p.current_pagparams(),
)
.await?;

paginator = p.found_batch(&batch, &|row| row.id);
paginator = p.found_batch(&batch, &|row| row.id.0.0);
sagas.append(&mut batch);
}
Ok(sagas)
Expand Down
100 changes: 83 additions & 17 deletions nexus/reconfigurator/planning/src/example.rs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for racing with the change here. Let me know if you want help merging it.

Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ pub struct ExampleSystemBuilder {
internal_dns_count: ZoneCount,
external_dns_count: ZoneCount,
crucible_pantry_count: ZoneCount,
boundary_ntp_count: ZoneCount,
create_zones: bool,
create_disks_in_blueprint: bool,
target_release: TargetReleaseDescription,
Expand Down Expand Up @@ -248,6 +249,9 @@ impl ExampleSystemBuilder {
internal_dns_count: ZoneCount(INTERNAL_DNS_REDUNDANCY),
external_dns_count: ZoneCount(Self::DEFAULT_EXTERNAL_DNS_COUNT),
crucible_pantry_count: ZoneCount(CRUCIBLE_PANTRY_REDUNDANCY),
// By default we only set up internal NTP; callers that specifically
// want boundary NTP can ask for them.
boundary_ntp_count: ZoneCount(0),
create_zones: true,
create_disks_in_blueprint: true,
target_release: TargetReleaseDescription::Initial,
Expand Down Expand Up @@ -327,6 +331,28 @@ impl ExampleSystemBuilder {
Ok(self)
}

/// Set the number of boundary NTP instances in the example system.
///
/// The default value is 0. A value anywhere between 0 and 30, inclusive, is
/// permitted. (The limit of 30 is primarily to simplify the
/// implementation.)
///
/// Each NTP server is assigned an external SNAT address in the 198.51.100.x
/// range.
pub fn set_boundary_ntp_count(
mut self,
boundary_ntp_count: usize,
) -> anyhow::Result<Self> {
if boundary_ntp_count > 30 {
anyhow::bail!(
"boundary_ntp_count {} is greater than 30",
boundary_ntp_count,
);
}
self.boundary_ntp_count = ZoneCount(boundary_ntp_count);
Ok(self)
}

/// Set the number of Crucible pantry instances in the example system.
///
/// If [`Self::create_zones`] is set to `false`, this is ignored.
Expand Down Expand Up @@ -428,6 +454,10 @@ impl ExampleSystemBuilder {
self.external_dns_count.0
}

pub fn boundary_ntp_zones(&self) -> usize {
self.boundary_ntp_count.0
}

/// Create a new example system with the given modifications.
///
/// Return the system, and the initial blueprint that matches it.
Expand All @@ -443,6 +473,7 @@ impl ExampleSystemBuilder {
"internal_dns_count" => self.internal_dns_count.0,
"external_dns_count" => self.external_dns_count.0,
"crucible_pantry_count" => self.crucible_pantry_count.0,
"boundary_ntp_count" => self.boundary_ntp_count.0,
"create_zones" => self.create_zones,
"create_disks_in_blueprint" => self.create_disks_in_blueprint,
);
Expand Down Expand Up @@ -517,19 +548,34 @@ impl ExampleSystemBuilder {
.unwrap(),
)
.unwrap();
for i in 0..self.external_dns_count.0 {
let lo = (i + 1)
.try_into()
.expect("external_dns_count is always <= 30");
for i in 1..=self.external_dns_count.0 {
let ip = format!("198.51.100.{i}");
builder
.add_external_dns_ip(IpAddr::V4(Ipv4Addr::new(
198, 51, 100, lo,
)))
.add_external_dns_ip(ip.parse().unwrap())
.expect("test IPs are valid service IPs");
}
system.set_external_ip_policy(builder.build());
}

// Also add a 30-ip range for boundary NTP. It's very likely we'll
// actually allocate out of the leftover external DNS range, but if
// someone actually asked for 30 external DNS zones we'll shift up into
// this range.
if self.boundary_ntp_count.0 > 0 {
let mut builder =
system.external_ip_policy().clone().into_builder();
builder
.push_service_pool_ipv4_range(
Ipv4Range::new(
"198.51.100.31".parse::<Ipv4Addr>().unwrap(),
"198.51.100.60".parse::<Ipv4Addr>().unwrap(),
)
.unwrap(),
)
.unwrap();
system.set_external_ip_policy(builder.build());
}

let mut input_builder = system
.to_planning_input_builder(Arc::new(
// Start with an empty blueprint.
Expand Down Expand Up @@ -565,23 +611,43 @@ impl ExampleSystemBuilder {
// * Create disks and non-discretionary zones on all sleds.
// * Only create discretionary zones on discretionary sleds.
let mut discretionary_ix = 0;
for (sled_id, sled_details) in
base_input.all_sleds(SledFilter::Commissioned)
for (sled_idx, (sled_id, sled_details)) in
base_input.all_sleds(SledFilter::Commissioned).enumerate()
{
if self.create_disks_in_blueprint {
let _ = builder
.sled_add_disks(sled_id, &sled_details.resources)
.unwrap();
}
if self.create_zones {
let _ = builder
.sled_ensure_zone_ntp(
sled_id,
self.target_release
.zone_image_source(ZoneKind::BoundaryNtp)
.expect("obtained BoundaryNtp image source"),
)
.unwrap();
// Add boundary NTP to the first `self.boundary_ntp_count` sleds
// and internal NTP to the rest.
if sled_idx < self.boundary_ntp_count.0 {
let external_ip = external_networking_alloc
.for_new_boundary_ntp()
.expect("should have an external IP for boundary NTP");
builder
.sled_add_zone_boundary_ntp_with_config(
sled_id,
vec!["ntp.oxide.computer".to_string()],
vec!["1.1.1.1".parse().unwrap()],
None,
self.target_release
.zone_image_source(ZoneKind::BoundaryNtp)
.expect("obtained BoundaryNtp image source"),
external_ip,
)
.unwrap();
} else {
let _ = builder
.sled_ensure_zone_ntp(
sled_id,
self.target_release
.zone_image_source(ZoneKind::BoundaryNtp)
.expect("obtained BoundaryNtp image source"),
)
.unwrap();
}

// Create discretionary zones if allowed.
if sled_details.policy.matches(SledFilter::Discretionary) {
Expand Down
13 changes: 13 additions & 0 deletions nexus/reconfigurator/preparation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,18 @@ sled-agent-types.workspace = true
sled-hardware-types.workspace = true
slog.workspace = true
slog-error-chain.workspace = true
strum.workspace = true

omicron-workspace-hack.workspace = true

[dev-dependencies]
chrono.workspace = true
clickhouse-admin-types.workspace = true
nexus-auth.workspace = true
nexus-reconfigurator-planning.workspace = true
nexus-test-utils.workspace = true
omicron-test-utils.workspace = true
serde_json.workspace = true
steno.workspace = true
tokio.workspace = true
uuid.workspace = true
25 changes: 25 additions & 0 deletions nexus/reconfigurator/preparation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ use std::collections::BTreeSet;
use std::net::IpAddr;
use std::sync::Arc;

mod pruneable_zones;

use pruneable_zones::PruneableZones;

/// Given various pieces of database state that go into the blueprint planning
/// process, produce a `PlanningInput` object encapsulating what the planner
/// needs to generate a blueprint
Expand Down Expand Up @@ -92,6 +96,7 @@ pub struct PlanningInputFromDb<'a> {
pub planner_config: PlannerConfig,
pub active_nexus_zones: BTreeSet<OmicronZoneUuid>,
pub not_yet_nexus_zones: BTreeSet<OmicronZoneUuid>,
pub pruneable_zones: BTreeSet<OmicronZoneUuid>,
pub log: &'a Logger,
}

Expand Down Expand Up @@ -269,6 +274,15 @@ impl PlanningInputFromDb<'_> {
active_nexus_zones.into_iter().map(|n| n.nexus_id()).collect();
let not_yet_nexus_zones =
not_yet_nexus_zones.into_iter().map(|n| n.nexus_id()).collect();
let pruneable_zones = PruneableZones::new(
opctx,
datastore,
&parent_blueprint,
&external_ip_rows,
&service_nic_rows,
)
.await?
.into_pruneable_zones();

let planning_input = PlanningInputFromDb {
parent_blueprint,
Expand Down Expand Up @@ -297,6 +311,7 @@ impl PlanningInputFromDb<'_> {
planner_config,
active_nexus_zones,
not_yet_nexus_zones,
pruneable_zones,
}
.build()
.internal_context("assembling planning_input")?;
Expand Down Expand Up @@ -456,6 +471,16 @@ impl PlanningInputFromDb<'_> {
})?;
}

for &zone_id in &self.pruneable_zones {
builder.insert_pruneable_zone(zone_id).map_err(|e| {
Error::internal_error(&format!(
"unexpectedly failed to pruneable zone ID {zone_id} \
to planning input: {}",
InlineErrorChain::new(&e),
))
})?;
}

Ok(builder.build())
}
}
Expand Down
Loading
Loading