Skip to content

Commit 38b0fd4

Browse files
committed
[spr] initial version
Created using spr 1.3.6-beta.1
2 parents 165b854 + 83df7d9 commit 38b0fd4

File tree

13 files changed

+409
-6
lines changed

13 files changed

+409
-6
lines changed

dev-tools/reconfigurator-cli/tests/output/cmds-add-sled-no-disks-stdout

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ generated inventory collection eb0796d5-ab8a-4f7b-a884-b4aeacb8ab51 from configu
3636
> # Try to plan a new blueprint; this should be okay even though the sled
3737
> # we added has no disks.
3838
> blueprint-plan dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 eb0796d5-ab8a-4f7b-a884-b4aeacb8ab51
39+
INFO skipping noop image source check for all sleds (no current TUF repo)
3940
INFO skipping sled (no zpools in service), sled_id: 00320471-945d-413c-85e7-03e091a70b3c
4041
INFO sufficient BoundaryNtp zones exist in plan, desired_count: 0, current_count: 0
4142
INFO sufficient Clickhouse zones exist in plan, desired_count: 1, current_count: 1

dev-tools/reconfigurator-cli/tests/output/cmds-example-stdout

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@ T ENA ID PARENT
493493
* yes ade5749d-bdf3-4fab-a8ae-00bea01b3a5a 02697f74-b14a-4418-90f0-c28b2a3a6aa9 <REDACTED_TIMESTAMP>
494494

495495
> blueprint-plan ade5749d-bdf3-4fab-a8ae-00bea01b3a5a
496+
INFO skipping noop image source check for all sleds (no current TUF repo)
496497
INFO found sled missing NTP zone (will add one), sled_id: 89d02b1b-478c-401a-8e28-7a26f74fa41b
497498
INFO sufficient BoundaryNtp zones exist in plan, desired_count: 0, current_count: 0
498499
WARN failed to place all new desired Clickhouse zones, placed: 0, wanted_to_place: 1

dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-external-dns-stdout

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,7 @@ parent: 3f00b694-1b16-4aaa-8f78-e6b3a527b434
969969

970970
> # blueprint-plan will place a new external DNS zone, diff DNS to see the new zone has `ns<N>` and NS records.
971971
> blueprint-plan 366b0b68-d80e-4bc1-abd3-dc69837847e0
972+
INFO skipping noop image source check for all sleds (no current TUF repo)
972973
INFO sufficient BoundaryNtp zones exist in plan, desired_count: 0, current_count: 0
973974
INFO sufficient Clickhouse zones exist in plan, desired_count: 1, current_count: 1
974975
INFO sufficient ClickhouseKeeper zones exist in plan, desired_count: 0, current_count: 0

dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-internal-dns-stdout

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,7 @@ external DNS:
10021002

10031003
> # Planning a new blueprint will now replace the expunged zone, with new records for its replacement.
10041004
> blueprint-plan 58d5e830-0884-47d8-a7cd-b2b3751adeb4
1005+
INFO skipping noop image source check for all sleds (no current TUF repo)
10051006
INFO sufficient BoundaryNtp zones exist in plan, desired_count: 0, current_count: 0
10061007
INFO sufficient Clickhouse zones exist in plan, desired_count: 1, current_count: 1
10071008
INFO sufficient ClickhouseKeeper zones exist in plan, desired_count: 0, current_count: 0

nexus/db-model/src/target_release.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,13 @@ impl TargetRelease {
6464
pub fn into_external(
6565
&self,
6666
release_source: views::TargetReleaseSource,
67+
mupdate_override: bool,
6768
) -> views::TargetRelease {
6869
views::TargetRelease {
6970
generation: (&self.generation.0).into(),
7071
time_requested: self.time_requested,
7172
release_source,
73+
mupdate_override,
7274
}
7375
}
7476
}

nexus/db-queries/src/db/datastore/deployment.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1560,6 +1560,23 @@ impl DataStore {
15601560
Self::blueprint_current_target_only(&conn).await.map_err(|e| e.into())
15611561
}
15621562

1563+
/// Get the minimum generation for the current target blueprint, if one exists
1564+
pub async fn blueprint_target_get_current_min_gen(
1565+
&self,
1566+
opctx: &OpContext,
1567+
) -> Result<Generation, Error> {
1568+
opctx.authorize(authz::Action::Read, &authz::BLUEPRINT_CONFIG).await?;
1569+
let conn = self.pool_connection_authorized(opctx).await?;
1570+
let target = Self::blueprint_current_target_only(&conn).await?;
1571+
1572+
let authz_blueprint = authz_blueprint_from_id(target.target_id);
1573+
Self::blueprint_get_minimum_generation_connection(
1574+
&authz_blueprint,
1575+
&conn,
1576+
)
1577+
.await
1578+
}
1579+
15631580
// Helper to fetch the current blueprint target (without fetching the entire
15641581
// blueprint for that target).
15651582
//
@@ -1587,6 +1604,28 @@ impl DataStore {
15871604

15881605
Ok(current_target.into())
15891606
}
1607+
1608+
// Helper to fetch the minimum generation for a blueprint ID (without
1609+
// fetching the entire blueprint for that ID.)
1610+
async fn blueprint_get_minimum_generation_connection(
1611+
authz: &authz::Blueprint,
1612+
conn: &async_bb8_diesel::Connection<DbConnection>,
1613+
) -> Result<Generation, Error> {
1614+
use nexus_db_schema::schema::blueprint::dsl;
1615+
1616+
let id = authz.id();
1617+
let db_blueprint = dsl::blueprint
1618+
.filter(dsl::id.eq(id))
1619+
.select(DbBlueprint::as_select())
1620+
.first_async::<DbBlueprint>(conn)
1621+
.await
1622+
.optional()
1623+
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;
1624+
let db_blueprint = db_blueprint.ok_or_else(|| {
1625+
Error::not_found_by_id(ResourceType::Blueprint, &id)
1626+
})?;
1627+
Ok(db_blueprint.target_release_minimum_generation.0)
1628+
}
15901629
}
15911630

15921631
// Helper to create an `authz::Blueprint` for a specific blueprint ID

nexus/db-queries/src/db/datastore/target_release.rs

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,15 @@ impl DataStore {
125125
}
126126
}
127127
};
128-
Ok(target_release.into_external(release_source))
128+
// We choose to fetch the blueprint directly from the database rather
129+
// than relying on the cached blueprint in Nexus because our APIs try to
130+
// be strongly consistent. This shows up/will show up as a warning in
131+
// the UI, and we don't want the warning to flicker in and out of
132+
// existence based on which Nexus is getting hit.
133+
let min_gen = self.blueprint_target_get_current_min_gen(opctx).await?;
134+
// The semantics of min_gen mean we use a > sign here, not >=.
135+
let mupdate_override = min_gen > target_release.generation.0;
136+
Ok(target_release.into_external(release_source, mupdate_override))
129137
}
130138
}
131139

@@ -135,6 +143,12 @@ mod test {
135143
use crate::db::model::{Generation, TargetReleaseSource};
136144
use crate::db::pub_test_utils::TestDatabase;
137145
use chrono::{TimeDelta, Utc};
146+
use nexus_inventory::now_db_precision;
147+
use nexus_reconfigurator_planning::blueprint_builder::BlueprintBuilder;
148+
use nexus_reconfigurator_planning::example::{
149+
ExampleSystemBuilder, SimRngState,
150+
};
151+
use nexus_types::deployment::BlueprintTarget;
138152
use omicron_common::api::external::{
139153
TufArtifactMeta, TufRepoDescription, TufRepoMeta,
140154
};
@@ -145,7 +159,8 @@ mod test {
145159

146160
#[tokio::test]
147161
async fn target_release_datastore() {
148-
let logctx = dev::test_setup_log("target_release_datastore");
162+
const TEST_NAME: &str = "target_release_datastore";
163+
let logctx = dev::test_setup_log(TEST_NAME);
149164
let db = TestDatabase::new_with_datastore(&logctx.log).await;
150165
let (opctx, datastore) = (db.opctx(), db.datastore());
151166

@@ -163,6 +178,56 @@ mod test {
163178
);
164179
assert!(initial_target_release.tuf_repo_id.is_none());
165180

181+
// Set up an initial blueprint and make it the target. This models real
182+
// systems which always have a target blueprint.
183+
let mut rng = SimRngState::from_seed(TEST_NAME);
184+
let (system, mut blueprint) = ExampleSystemBuilder::new_with_rng(
185+
&logctx.log,
186+
rng.next_system_rng(),
187+
)
188+
.build();
189+
assert_eq!(
190+
blueprint.target_release_minimum_generation,
191+
1.into(),
192+
"initial blueprint should have minimum generation of 1",
193+
);
194+
// Treat this blueprint as the initial one for the system.
195+
blueprint.parent_blueprint_id = None;
196+
197+
datastore
198+
.blueprint_insert(&opctx, &blueprint)
199+
.await
200+
.expect("inserted blueprint");
201+
datastore
202+
.blueprint_target_set_current(
203+
opctx,
204+
BlueprintTarget {
205+
target_id: blueprint.id,
206+
// enabled = true or false shouldn't matter for this.
207+
enabled: true,
208+
time_made_target: now_db_precision(),
209+
},
210+
)
211+
.await
212+
.expect("set blueprint target");
213+
214+
// We should always be able to get a view of the target release.
215+
let initial_target_release_view = datastore
216+
.target_release_view(opctx, &initial_target_release)
217+
.await
218+
.expect("got target release");
219+
eprintln!(
220+
"initial target release view: {:#?}",
221+
initial_target_release_view
222+
);
223+
224+
// This target release should not have the mupdate override set, because
225+
// the generation is <= the minimum generation in the target blueprint.
226+
assert!(
227+
!initial_target_release_view.mupdate_override,
228+
"mupdate_override should be false for initial target release"
229+
);
230+
166231
// We should be able to set a new generation just like the first.
167232
// We allow some slack in the timestamp comparison because the
168233
// database only stores timestamps with μsec precision.
@@ -256,6 +321,86 @@ mod test {
256321
);
257322
assert_eq!(target_release.tuf_repo_id, Some(tuf_repo_id));
258323

324+
// Generate a new blueprint with a greater target release generation.
325+
let mut builder = BlueprintBuilder::new_based_on(
326+
&logctx.log,
327+
&blueprint,
328+
&system.input,
329+
&system.collection,
330+
TEST_NAME,
331+
)
332+
.expect("created blueprint builder");
333+
builder.set_rng(rng.next_planner_rng());
334+
builder
335+
.set_target_release_minimum_generation(
336+
blueprint.target_release_minimum_generation,
337+
5.into(),
338+
)
339+
.expect("set target release minimum generation");
340+
let bp2 = builder.build();
341+
342+
datastore
343+
.blueprint_insert(&opctx, &bp2)
344+
.await
345+
.expect("inserted blueprint");
346+
datastore
347+
.blueprint_target_set_current(
348+
opctx,
349+
BlueprintTarget {
350+
target_id: bp2.id,
351+
// enabled = true or false shouldn't matter for this.
352+
enabled: true,
353+
time_made_target: now_db_precision(),
354+
},
355+
)
356+
.await
357+
.expect("set blueprint target");
358+
359+
// Fetch the target release again.
360+
let target_release = datastore
361+
.target_release_get_current(opctx)
362+
.await
363+
.expect("got target release");
364+
let target_release_view_2 = datastore
365+
.target_release_view(opctx, &target_release)
366+
.await
367+
.expect("got target release");
368+
369+
eprintln!("target release view 2: {target_release_view_2:#?}");
370+
371+
assert!(
372+
target_release_view_2.mupdate_override,
373+
"mupdate override is set",
374+
);
375+
376+
// Now set the target release again -- this should cause the mupdate
377+
// override to disappear.
378+
let before = Utc::now();
379+
let target_release = datastore
380+
.target_release_insert(
381+
opctx,
382+
TargetRelease::new_system_version(&target_release, tuf_repo_id),
383+
)
384+
.await
385+
.unwrap();
386+
let after = Utc::now();
387+
388+
assert_eq!(target_release.generation, Generation(5.into()));
389+
assert!(target_release.time_requested >= before);
390+
assert!(target_release.time_requested <= after);
391+
392+
let target_release_view_3 = datastore
393+
.target_release_view(opctx, &target_release)
394+
.await
395+
.expect("got target release");
396+
397+
eprintln!("target release view 3: {target_release_view_3:#?}");
398+
399+
assert!(
400+
!target_release_view_3.mupdate_override,
401+
"mupdate override is not set",
402+
);
403+
259404
// Clean up.
260405
db.terminate().await;
261406
logctx.cleanup_successful();

nexus/reconfigurator/planning/src/blueprint_builder/builder.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,10 @@ pub(crate) enum Operation {
303303
num_datasets_expunged: usize,
304304
num_zones_expunged: usize,
305305
},
306+
SledNoopZoneImageSourcesUpdated {
307+
sled_id: SledUuid,
308+
count: usize,
309+
},
306310
}
307311

308312
impl fmt::Display for Operation {
@@ -367,6 +371,13 @@ impl fmt::Display for Operation {
367371
{num_zones_expunged} zones)"
368372
)
369373
}
374+
Self::SledNoopZoneImageSourcesUpdated { sled_id, count } => {
375+
write!(
376+
f,
377+
"sled {sled_id}: performed {count} noop \
378+
zone image source updates"
379+
)
380+
}
370381
}
371382
}
372383
}
@@ -1133,6 +1144,20 @@ impl<'a> BlueprintBuilder<'a> {
11331144
Ok(datasets.into())
11341145
}
11351146

1147+
/// Returns the remove_mupdate_override field for a sled.
1148+
pub(crate) fn sled_get_remove_mupdate_override(
1149+
&self,
1150+
sled_id: SledUuid,
1151+
) -> Result<Option<MupdateOverrideUuid>, Error> {
1152+
let editor = self.sled_editors.get(&sled_id).ok_or_else(|| {
1153+
Error::Planner(anyhow!(
1154+
"tried to get remove_mupdate_override for \
1155+
unknown sled {sled_id}"
1156+
))
1157+
})?;
1158+
Ok(editor.get_remove_mupdate_override())
1159+
}
1160+
11361161
fn next_internal_dns_gz_address_index(&self, sled_id: SledUuid) -> u32 {
11371162
let used_internal_dns_gz_address_indices = self
11381163
.current_sled_zones(

nexus/reconfigurator/planning/src/blueprint_editor/sled_editor.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,18 @@ impl SledEditor {
286286
}
287287
}
288288

289+
/// Returns the remove_mupdate_override field for this sled.
290+
pub fn get_remove_mupdate_override(&self) -> Option<MupdateOverrideUuid> {
291+
match &self.0 {
292+
InnerSledEditor::Active(editor) => {
293+
*editor.remove_mupdate_override.value()
294+
}
295+
InnerSledEditor::Decommissioned(sled) => {
296+
sled.config.remove_mupdate_override
297+
}
298+
}
299+
}
300+
289301
fn as_active_mut(
290302
&mut self,
291303
) -> Result<&mut ActiveSledEditor, SledEditError> {
@@ -343,8 +355,6 @@ impl SledEditor {
343355
}
344356

345357
/// Sets the image source for a zone.
346-
///
347-
/// Currently only used by test code.
348358
pub fn set_zone_image_source(
349359
&mut self,
350360
zone_id: &OmicronZoneUuid,

nexus/reconfigurator/planning/src/blueprint_editor/sled_editor/scalar.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ impl<T> ScalarEditor<T> {
1616
ScalarEditor { original, value: EditValue::Original }
1717
}
1818

19-
#[expect(dead_code)]
2019
pub(crate) fn value(&self) -> &T {
2120
match &self.value {
2221
EditValue::Original => &self.original,

0 commit comments

Comments
 (0)