Skip to content

Commit 83df7d9

Browse files
committed
[spr] changes to main this commit is based on
Created using spr 1.3.6-beta.1 [skip ci]
1 parent 165b854 commit 83df7d9

File tree

6 files changed

+205
-3
lines changed

6 files changed

+205
-3
lines changed

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/planner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ impl<'a> Planner<'a> {
146146

147147
fn do_plan(&mut self) -> Result<(), Error> {
148148
self.do_plan_expunge()?;
149-
self.do_plan_add()?;
150149
self.do_plan_decommission()?;
150+
self.do_plan_add()?;
151151
if let UpdateStepResult::ContinueToNextStep = self.do_plan_mgs_updates()
152152
{
153153
self.do_plan_zone_updates()?;

nexus/types/src/external_api/views.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1488,6 +1488,17 @@ pub struct TargetRelease {
14881488

14891489
/// The source of the target release.
14901490
pub release_source: TargetReleaseSource,
1491+
1492+
/// If true, indicates that at least one sled in the system has been updated
1493+
/// through the recovery (MUPdate) path since the last time the target
1494+
/// release was set.
1495+
///
1496+
/// In this case, the system will ignore the currently-set target release,
1497+
/// on the assumption that continuing an update may reintroduce or
1498+
/// exacerbate whatever problem caused the recovery path to be used. An
1499+
/// operator must set the target release again in order to resume automated
1500+
/// updates.
1501+
pub mupdate_override: bool,
14911502
}
14921503

14931504
fn expected_one_of<T: strum::VariantArray + fmt::Display>() -> String {

openapi/nexus.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24802,6 +24802,10 @@
2480224802
"type": "integer",
2480324803
"format": "int64"
2480424804
},
24805+
"mupdate_override": {
24806+
"description": "If true, indicates that at least one sled in the system has been updated through the recovery (MUPdate) path since the last time the target release was set.\n\nIn this case, the system will ignore the currently-set target release, on the assumption that continuing an update may reintroduce or exacerbate whatever problem caused the recovery path to be used. An operator must set the target release again in order to resume automated updates.",
24807+
"type": "boolean"
24808+
},
2480524809
"release_source": {
2480624810
"description": "The source of the target release.",
2480724811
"allOf": [
@@ -24818,6 +24822,7 @@
2481824822
},
2481924823
"required": [
2482024824
"generation",
24825+
"mupdate_override",
2482124826
"release_source",
2482224827
"time_requested"
2482324828
]

0 commit comments

Comments
 (0)