Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ba8676a
[spr] initial version
sunshowers Jun 26, 2025
5ff56b6
[spr] changes to main this commit is based on
sunshowers Jun 26, 2025
97e3c59
clippy
sunshowers Jun 26, 2025
7a9908b
[spr] changes introduced through rebase
sunshowers Jul 8, 2025
3638cf8
rebase, mostly ready for review
sunshowers Jul 8, 2025
99668b1
rebase on 28, comments, pending MGS updates
sunshowers Jul 10, 2025
ac482d8
[spr] changes introduced through rebase
sunshowers Jul 10, 2025
e506a84
update comment
sunshowers Jul 10, 2025
ca33786
clippy
sunshowers Jul 10, 2025
7bf9efa
[spr] changes introduced through rebase
sunshowers Jul 10, 2025
de1f2c0
updates
sunshowers Jul 15, 2025
cd379e8
[spr] changes introduced through rebase
sunshowers Jul 15, 2025
e6f7978
rebase on main
sunshowers Jul 16, 2025
eda5af1
[spr] changes introduced through rebase
sunshowers Jul 16, 2025
0f42970
rebase on 39
sunshowers Jul 25, 2025
dd89dd0
[spr] changes introduced through rebase
sunshowers Jul 25, 2025
d763df1
fix logic
sunshowers Jul 25, 2025
660a867
use debug rather than info to reduce reconfigurator-cli noise
sunshowers Jul 25, 2025
a6a0e77
rebase
sunshowers Jul 25, 2025
3b2cdb2
[spr] changes introduced through rebase
sunshowers Jul 25, 2025
26f5626
rebase
sunshowers Jul 29, 2025
796a0e5
[spr] changes introduced through rebase
sunshowers Jul 29, 2025
2f086af
add chicken switch tests
sunshowers Jul 29, 2025
d15692e
[spr] changes introduced through rebase
smklein Jul 30, 2025
d146cbc
more changes + qs
sunshowers Jul 30, 2025
f64a195
update comment
sunshowers Jul 30, 2025
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
2 changes: 2 additions & 0 deletions Cargo.lock

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

40 changes: 40 additions & 0 deletions dev-tools/reconfigurator-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ fn process_command(
Commands::SledShow(args) => cmd_sled_show(sim, args),
Commands::SledSetPolicy(args) => cmd_sled_set_policy(sim, args),
Commands::SledUpdateSp(args) => cmd_sled_update_sp(sim, args),
Commands::SledSetMupdateOverride(args) => {
cmd_sled_set_mupdate_override(sim, args)
}
Commands::SiloList => cmd_silo_list(sim),
Commands::SiloAdd(args) => cmd_silo_add(sim, args),
Commands::SiloRemove(args) => cmd_silo_remove(sim, args),
Expand Down Expand Up @@ -265,6 +268,8 @@ enum Commands {
SledSetPolicy(SledSetPolicyArgs),
/// simulate updating the sled's SP versions
SledUpdateSp(SledUpdateSpArgs),
/// simulate the sled's mupdate override field changing
SledSetMupdateOverride(SledSetMupdateOverrideArgs),

/// list silos
SiloList,
Expand Down Expand Up @@ -390,6 +395,15 @@ struct SledUpdateSpArgs {
inactive: Option<ExpectedVersion>,
}

#[derive(Debug, Args)]
struct SledSetMupdateOverrideArgs {
/// id of the sled
sled_id: SledUuid,

/// the new value of the mupdate override, or "unset"
mupdate_override_id: MupdateOverrideUuidOpt,
}

#[derive(Debug, Args)]
struct SledRemoveArgs {
/// id of the sled
Expand Down Expand Up @@ -993,6 +1007,32 @@ fn cmd_sled_update_sp(
)))
}

fn cmd_sled_set_mupdate_override(
sim: &mut ReconfiguratorSim,
args: SledSetMupdateOverrideArgs,
) -> anyhow::Result<Option<String>> {
let mut state = sim.current_state().to_mut();
state.system_mut().description_mut().sled_set_mupdate_override(
args.sled_id,
args.mupdate_override_id.into(),
)?;

let desc = match args.mupdate_override_id {
MupdateOverrideUuidOpt::Set(id) => format!("set to {id}"),
MupdateOverrideUuidOpt::Unset => "unset".to_owned(),
};

sim.commit_and_bump(
format!(
"reconfigurator-cli sled-set-mupdate-override: {}: {}",
args.sled_id, desc,
),
state,
);

Ok(Some(format!("set sled {} mupdate override: {}", args.sled_id, desc)))
}

fn cmd_inventory_list(
sim: &mut ReconfiguratorSim,
) -> anyhow::Result<Option<String>> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Load an example system.

load-example --nsleds 3 --ndisks-per-sled 1

# Set one of the sled's zone's image sources to a specific artifact.
blueprint-edit latest set-zone-image 466a9f29-62bf-4e63-924a-b9efdb86afec artifact 1.2.3 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

# Simulate a mupdate on sled 0 by setting the mupdate override field to a
# new UUID (generated using uuidgen).
sled-set-mupdate-override 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c 6123eac1-ec5b-42ba-b73f-9845105a9971

# Generate a new inventory and plan against that.
inventory-generate
blueprint-plan latest eb0796d5-ab8a-4f7b-a884-b4aeacb8ab51

# Diff the blueprints. This diff should show:
#
# * for sled 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c, "+ will remove mupdate override"
# * for zone 466a9f29-62bf-4e63-924a-b9efdb86afec, a change from artifact to install-dataset
blueprint-diff 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 latest

# Now simulate the new config being applied to the sled, which would
# cause the mupdate override to be removed.
sled-set-mupdate-override 2b8f0cb3-0295-4b3c-bc58-4fe88b57112c unset

# Generate a new inventory and plan against that.
inventory-generate
blueprint-plan latest 61f451b3-2121-4ed6-91c7-a550054f6c21

# Diff the blueprints. This diff should show the "remove mupdate
# override" line going away.
# TODO: we do not yet reset the install dataset image back to
# the desired artifact version -- we should do that in the future.
blueprint-diff 58d5e830-0884-47d8-a7cd-b2b3751adeb4 latest

# TODO: set target release

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23
METADATA:
created by::::::::::::: reconfigurator-cli
created at::::::::::::: <REDACTED_TIMESTAMP>
comment:::::::::::::::: (none)
comment:::::::::::::::: updated target release minimum generation from 1 to 2
internal DNS version::: 1
external DNS version::: 1
target release min gen: 2
Expand Down
2 changes: 2 additions & 0 deletions nexus/db-model/src/target_release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,13 @@ impl TargetRelease {
pub fn into_external(
&self,
release_source: views::TargetReleaseSource,
mupdate_override: Option<views::TargetReleaseMupdateOverride>,
) -> views::TargetRelease {
views::TargetRelease {
generation: (&self.generation.0).into(),
time_requested: self.time_requested,
release_source,
mupdate_override,
}
}
}
39 changes: 39 additions & 0 deletions nexus/db-queries/src/db/datastore/deployment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,23 @@ impl DataStore {
Self::blueprint_current_target_only(&conn).await.map_err(|e| e.into())
}

/// Get the minimum generation for the current target blueprint, if one exists
pub async fn blueprint_target_get_current_min_gen(
&self,
opctx: &OpContext,
) -> Result<Generation, Error> {
opctx.authorize(authz::Action::Read, &authz::BLUEPRINT_CONFIG).await?;
let conn = self.pool_connection_authorized(opctx).await?;
let target = Self::blueprint_current_target_only(&conn).await?;

let authz_blueprint = authz_blueprint_from_id(target.target_id);
Self::blueprint_get_minimum_generation_connection(
&authz_blueprint,
&conn,
)
.await
}

// Helper to fetch the current blueprint target (without fetching the entire
// blueprint for that target).
//
Expand Down Expand Up @@ -1587,6 +1604,28 @@ impl DataStore {

Ok(current_target.into())
}

// Helper to fetch the minimum generation for a blueprint ID (without
// fetching the entire blueprint for that ID.)
async fn blueprint_get_minimum_generation_connection(
authz: &authz::Blueprint,
conn: &async_bb8_diesel::Connection<DbConnection>,
) -> Result<Generation, Error> {
use nexus_db_schema::schema::blueprint::dsl;

let id = authz.id();
let db_blueprint = dsl::blueprint
.filter(dsl::id.eq(id))
.select(DbBlueprint::as_select())
.first_async::<DbBlueprint>(conn)
.await
.optional()
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;
let db_blueprint = db_blueprint.ok_or_else(|| {
Error::not_found_by_id(ResourceType::Blueprint, &id)
})?;
Ok(db_blueprint.target_release_minimum_generation.0)
}
}

// Helper to create an `authz::Blueprint` for a specific blueprint ID
Expand Down
152 changes: 150 additions & 2 deletions nexus/db-queries/src/db/datastore/target_release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,21 @@ impl DataStore {
}
}
};
Ok(target_release.into_external(release_source))
// We choose to fetch the blueprint directly from the database rather
// than relying on the cached blueprint in Nexus because our APIs try to
// be strongly consistent. This shows up/will show up as a warning in
// the UI, and we don't want the warning to flicker in and out of
// existence based on which Nexus is getting hit.
let min_gen = self.blueprint_target_get_current_min_gen(opctx).await?;
// The semantics of min_gen mean we use a > sign here, not >=.
let mupdate_override = if min_gen > target_release.generation.0 {
Some(views::TargetReleaseMupdateOverride {
minimum_generation: (&min_gen).into(),
})
} else {
None
};
Ok(target_release.into_external(release_source, mupdate_override))
}
}

Expand All @@ -135,6 +149,12 @@ mod test {
use crate::db::model::{Generation, TargetReleaseSource};
use crate::db::pub_test_utils::TestDatabase;
use chrono::{TimeDelta, Utc};
use nexus_inventory::now_db_precision;
use nexus_reconfigurator_planning::blueprint_builder::BlueprintBuilder;
use nexus_reconfigurator_planning::example::{
ExampleSystemBuilder, SimRngState,
};
use nexus_types::deployment::BlueprintTarget;
use omicron_common::api::external::{
TufArtifactMeta, TufRepoDescription, TufRepoMeta,
};
Expand All @@ -145,7 +165,8 @@ mod test {

#[tokio::test]
async fn target_release_datastore() {
let logctx = dev::test_setup_log("target_release_datastore");
const TEST_NAME: &str = "target_release_datastore";
let logctx = dev::test_setup_log(TEST_NAME);
let db = TestDatabase::new_with_datastore(&logctx.log).await;
let (opctx, datastore) = (db.opctx(), db.datastore());

Expand All @@ -163,6 +184,56 @@ mod test {
);
assert!(initial_target_release.tuf_repo_id.is_none());

// Set up an initial blueprint and make it the target. This models real
// systems which always have a target blueprint.
let mut rng = SimRngState::from_seed(TEST_NAME);
let (system, mut blueprint) = ExampleSystemBuilder::new_with_rng(
&logctx.log,
rng.next_system_rng(),
)
.build();
assert_eq!(
blueprint.target_release_minimum_generation,
1.into(),
"initial blueprint should have minimum generation of 1",
);
// Treat this blueprint as the initial one for the system.
blueprint.parent_blueprint_id = None;

datastore
.blueprint_insert(&opctx, &blueprint)
.await
.expect("inserted blueprint");
datastore
.blueprint_target_set_current(
opctx,
BlueprintTarget {
target_id: blueprint.id,
// enabled = true or false shouldn't matter for this.
enabled: true,
time_made_target: now_db_precision(),
},
)
.await
.expect("set blueprint target");

// We should always be able to get a view of the target release.
let initial_target_release_view = datastore
.target_release_view(opctx, &initial_target_release)
.await
.expect("got target release");
eprintln!(
"initial target release view: {:#?}",
initial_target_release_view
);

// This target release should not have the mupdate override set, because
// the generation is <= the minimum generation in the target blueprint.
assert_eq!(
initial_target_release_view.mupdate_override, None,
"mupdate_override should be None for initial target release"
);

// We should be able to set a new generation just like the first.
// We allow some slack in the timestamp comparison because the
// database only stores timestamps with μsec precision.
Expand Down Expand Up @@ -256,6 +327,83 @@ mod test {
);
assert_eq!(target_release.tuf_repo_id, Some(tuf_repo_id));

// Generate a new blueprint with a greater target release generation.
let mut builder = BlueprintBuilder::new_based_on(
&logctx.log,
&blueprint,
&system.input,
&system.collection,
TEST_NAME,
)
.expect("created blueprint builder");
builder.set_rng(rng.next_planner_rng());
builder
.set_target_release_minimum_generation(
blueprint.target_release_minimum_generation,
5.into(),
)
.expect("set target release minimum generation");
let bp2 = builder.build();

datastore
.blueprint_insert(&opctx, &bp2)
.await
.expect("inserted blueprint");
datastore
.blueprint_target_set_current(
opctx,
BlueprintTarget {
target_id: bp2.id,
// enabled = true or false shouldn't matter for this.
enabled: true,
time_made_target: now_db_precision(),
},
)
.await
.expect("set blueprint target");

// Fetch the target release again.
let target_release = datastore
.target_release_get_current(opctx)
.await
.expect("got target release");
let target_release_view_2 = datastore
.target_release_view(opctx, &target_release)
.await
.expect("got target release");

eprintln!("target release view 2: {target_release_view_2:#?}");

assert_eq!(
target_release_view_2.mupdate_override,
Some(views::TargetReleaseMupdateOverride { minimum_generation: 5 })
);

// Now set the target release again -- this should cause the mupdate
// override to disappear.
let before = Utc::now();
let target_release = datastore
.target_release_insert(
opctx,
TargetRelease::new_system_version(&target_release, tuf_repo_id),
)
.await
.unwrap();
let after = Utc::now();

assert_eq!(target_release.generation, Generation(5.into()));
assert!(target_release.time_requested >= before);
assert!(target_release.time_requested <= after);

let target_release_view_3 = datastore
.target_release_view(opctx, &target_release)
.await
.expect("got target release");

eprintln!("target release view 3: {target_release_view_3:#?}");

assert_eq!(target_release_view_3.mupdate_override, None);

// Clean up.
db.terminate().await;
logctx.cleanup_successful();
Expand Down
2 changes: 2 additions & 0 deletions nexus/reconfigurator/planning/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ chrono.workspace = true
debug-ignore.workspace = true
daft.workspace = true
gateway-client.workspace = true
iddqd.workspace = true
id-map.workspace = true
illumos-utils.workspace = true
indexmap.workspace = true
Expand All @@ -36,6 +37,7 @@ slog-error-chain.workspace = true
sp-sim.workspace = true
static_assertions.workspace = true
strum.workspace = true
swrite.workspace = true
thiserror.workspace = true
tufaceous-artifact.workspace = true
typed-rng.workspace = true
Expand Down
Loading
Loading