Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
310551c
[reconfigurator] background task for stopping marked VMMs
karencfv Sep 30, 2026
e1f14c3
let's do batches
karencfv Oct 2, 2026
99e577d
fix some comments
karencfv Oct 5, 2026
cb3ea34
use IdOrdMap everywhere
karencfv Oct 5, 2026
b1a9a19
have nicer return types
karencfv Oct 5, 2026
055b6b0
pondering...
karencfv Oct 5, 2026
81b1f62
merge main
karencfv Oct 6, 2026
a8f3e4e
Do the stopping and add a bunch of random thoughts
karencfv Oct 6, 2026
787853b
merge main
karencfv Oct 7, 2026
f4fea86
integrate with vmm marking bg task
karencfv Oct 7, 2026
a91d7d1
handle all possible scenarios attempting to stop a VMM
karencfv Oct 7, 2026
56d116c
Do not change the intended state if we're stopping for an update
karencfv Oct 7, 2026
267ed82
better logging
karencfv Oct 7, 2026
5f3b05e
expectorate
karencfv Oct 7, 2026
bf73666
add test for task
karencfv Oct 8, 2026
3a62920
organise test
karencfv Oct 8, 2026
8c2fdae
clean up
karencfv Oct 8, 2026
6196cb9
add db-query test
karencfv Oct 8, 2026
9ef8b8e
clean up
karencfv Oct 8, 2026
97e2afc
formatting
karencfv Oct 12, 2026
ce41e5e
call notify_instance_updated
karencfv Oct 12, 2026
f076ccb
add comment
karencfv Oct 13, 2026
1a63679
change interval for running task
karencfv Oct 15, 2026
26cb9ac
remove nexus
karencfv Oct 15, 2026
26779a3
one reqwest client
karencfv Oct 15, 2026
bc3cb0f
typos and small fixes
karencfv Oct 19, 2026
14a620f
use paginated_multicolumn
karencfv Oct 19, 2026
c0154d4
put the client where it should go
karencfv Oct 19, 2026
9f418e3
parallel tasks
karencfv Oct 19, 2026
6d41c21
use BTreeSet and leave TODOs for tomorrow
karencfv Oct 19, 2026
e42049b
omfg
karencfv Oct 19, 2026
179a441
log the instance id
karencfv Oct 19, 2026
29db8ef
handle vmm_gone
karencfv Oct 20, 2026
f56309c
optimise the optimisation
karencfv Oct 20, 2026
a603bfa
clean up
karencfv Oct 20, 2026
80eafe6
improve documentation
karencfv Oct 20, 2026
a30797d
make field more explicit
karencfv Oct 20, 2026
2533d11
cache the reqwest client
karencfv Oct 20, 2026
73529c9
fix logs
karencfv Oct 21, 2026
1e77105
fix typos
karencfv Oct 21, 2026
7355d30
do not stop Creating VMMs
karencfv Oct 22, 2026
a0b22ca
only retrieve vmms in in-service sleds
karencfv Oct 22, 2026
6e18f90
adapt tests
karencfv Oct 22, 2026
63d3678
fix typos before John finds them
karencfv Oct 22, 2026
54357d1
address comments
karencfv Oct 27, 2026
304c2f8
improve testing on failures
karencfv Oct 27, 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
55 changes: 55 additions & 0 deletions dev-tools/omdb/src/bin/omdb/nexus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,9 @@ fn print_task_details(bgtask: &BackgroundTask, details: &serde_json::Value) {
"abandoned_vmm_reaper" => {
print_task_abandoned_vmm_reaper(details);
}
"vmm_stop_for_update" => {
print_task_vmm_stop_for_update(details);
}
"attached_subnet_manager" => {
print_task_attached_subnet_manager_status(details);
}
Expand Down Expand Up @@ -1510,6 +1513,58 @@ fn print_task_abandoned_vmm_reaper(details: &serde_json::Value) {
};
}

fn print_task_vmm_stop_for_update(details: &serde_json::Value) {
use nexus_types::internal_api::background::VmmStopForUpdateStatus;

match serde_json::from_value::<VmmStopForUpdateStatus>(details.clone()) {
Err(error) => eprintln!(
"warning: failed to interpret task details: {:?}: {:?}",
error, details
),
Ok(VmmStopForUpdateStatus {
vmms_stopped_by_sled,
vmms_failed_to_stop_by_sled,
error_messages,
}) => {
let total: usize =
vmms_stopped_by_sled.iter().map(|s| s.vmm_ids.len()).sum();
println!(" VMMs stopped for sled evacuation: {total} total");
for sled in vmms_stopped_by_sled {
println!(
" sled_id={} n_stopped={}",
sled.sled_id,
sled.vmm_ids.len()
);
}
if !vmms_failed_to_stop_by_sled.is_empty() {
let total_failed: usize = vmms_failed_to_stop_by_sled
.iter()
.map(|s| s.vmm_ids.len())
.sum();
println!(" VMMs that failed to stop: {total_failed} total");
for sled in vmms_failed_to_stop_by_sled {
println!(
" sled_id={} n_failed_to_stop={}",
sled.sled_id,
sled.vmm_ids.len()
);
for vmm in sled.vmm_ids {
println!(" vmm_id={}", vmm);
}
}
}

if !error_messages.is_empty() {
let total_err = error_messages.len();
println!(" Error messages: {total_err} total");
for error in error_messages {
println!(" {error}");
}
}
}
};
}

fn print_task_blueprint_planner(details: &serde_json::Value) {
let status =
match serde_json::from_value::<BlueprintPlannerStatus>(details.clone())
Expand Down
12 changes: 12 additions & 0 deletions dev-tools/omdb/tests/env.out
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ task: "vmm_mark_stop_for_update"
marks VMMs on evacuating sleds as needing to be stopped for an update


task: "vmm_stop_for_update"
stops VMMs that are marked to be stopped for a sled update


task: "vpc_route_manager"
propagates updated VPC routes to all OPTE ports

Expand Down Expand Up @@ -547,6 +551,10 @@ task: "vmm_mark_stop_for_update"
marks VMMs on evacuating sleds as needing to be stopped for an update


task: "vmm_stop_for_update"
stops VMMs that are marked to be stopped for a sled update


task: "vpc_route_manager"
propagates updated VPC routes to all OPTE ports

Expand Down Expand Up @@ -810,6 +818,10 @@ task: "vmm_mark_stop_for_update"
marks VMMs on evacuating sleds as needing to be stopped for an update


task: "vmm_stop_for_update"
stops VMMs that are marked to be stopped for a sled update


task: "vpc_route_manager"
propagates updated VPC routes to all OPTE ports

Expand Down
16 changes: 16 additions & 0 deletions dev-tools/omdb/tests/successes.out
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,10 @@ task: "vmm_mark_stop_for_update"
marks VMMs on evacuating sleds as needing to be stopped for an update


task: "vmm_stop_for_update"
stops VMMs that are marked to be stopped for a sled update


task: "vpc_route_manager"
propagates updated VPC routes to all OPTE ports

Expand Down Expand Up @@ -1209,6 +1213,12 @@ task: "vmm_mark_stop_for_update"
batch size: 1000
VMMs marked to be stopped for an update: 0

task: "vmm_stop_for_update"
configured period: every <REDACTED_DURATION>m
last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED>
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
VMMs stopped for sled evacuation: 0 total

task: "vpc_route_manager"
configured period: every <REDACTED_DURATION>s
last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED>
Expand Down Expand Up @@ -1946,6 +1956,12 @@ task: "vmm_mark_stop_for_update"
batch size: 1000
VMMs marked to be stopped for an update: 0

task: "vmm_stop_for_update"
configured period: every <REDACTED_DURATION>m
last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED>
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
VMMs stopped for sled evacuation: 0 total

task: "vpc_route_manager"
configured period: every <REDACTED_DURATION>s
last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED>
Expand Down
16 changes: 16 additions & 0 deletions nexus-config/src/nexus_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,9 @@ pub struct BackgroundTaskConfig {
pub v2p_mapping_propagation: V2PMappingPropagationConfig,
/// configuration for abandoned VMM reaper task
pub abandoned_vmm_reaper: AbandonedVmmReaperConfig,
/// configuration for the task that stops VMMs marked to be stopped for an
/// update
pub vmm_stop_for_update: VmmStopForUpdateConfig,
/// configuration for saga recovery task
pub saga_recovery: SagaRecoveryConfig,
/// configuration for lookup region port task
Expand Down Expand Up @@ -825,6 +828,14 @@ pub struct AbandonedVmmReaperConfig {
pub period_secs: Duration,
}

#[serde_as]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct VmmStopForUpdateConfig {
/// period (in seconds) for periodic activations of this background task
#[serde_as(as = "DurationSeconds<u64>")]
pub period_secs: Duration,
}

#[serde_as]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct SagaRecoveryConfig {
Expand Down Expand Up @@ -1387,6 +1398,7 @@ mod test {
service_firewall_propagation.period_secs = 300
v2p_mapping_propagation.period_secs = 30
abandoned_vmm_reaper.period_secs = 60
vmm_stop_for_update.period_secs = 300
saga_recovery.period_secs = 60
lookup_region_port.period_secs = 60
region_snapshot_replacement_start.period_secs = 30
Expand Down Expand Up @@ -1627,6 +1639,9 @@ mod test {
abandoned_vmm_reaper: AbandonedVmmReaperConfig {
period_secs: Duration::from_secs(60),
},
vmm_stop_for_update: VmmStopForUpdateConfig {
period_secs: Duration::from_secs(300),
},
saga_recovery: SagaRecoveryConfig {
period_secs: Duration::from_secs(60),
},
Expand Down Expand Up @@ -1803,6 +1818,7 @@ mod test {
service_firewall_propagation.period_secs = 300
v2p_mapping_propagation.period_secs = 30
abandoned_vmm_reaper.period_secs = 60
vmm_stop_for_update.period_secs = 300
saga_recovery.period_secs = 60
lookup_region_port.period_secs = 60
region_snapshot_replacement_start.period_secs = 30
Expand Down
1 change: 1 addition & 0 deletions nexus/background-task-interface/src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub struct BackgroundTasks {
pub task_instance_reincarnation: Activator,
pub task_service_firewall_propagation: Activator,
pub task_abandoned_vmm_reaper: Activator,
pub task_vmm_stop_for_update: Activator,
pub task_audit_log_cleanup: Activator,
pub task_audit_log_timeout_incomplete: Activator,
pub task_vpc_route_manager: Activator,
Expand Down
123 changes: 123 additions & 0 deletions nexus/db-queries/src/db/datastore/vmm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::db::model;
use crate::db::model::Vmm;
use crate::db::model::VmmState as DbVmmState;
use crate::db::pagination::paginated;
use crate::db::pagination::paginated_multicolumn;
use crate::db::update_and_check::UpdateAndCheck;
use crate::db::update_and_check::UpdateAndQueryResult;
use crate::db::update_and_check::UpdateStatus;
Expand Down Expand Up @@ -569,6 +570,26 @@ impl DataStore {
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
}

/// Lists VMMs that have been marked as needing to be stopped for a sled
/// update.
///
/// The query performed by this function is paginated by the sled and
/// VMM UUIDs, in that order.
pub async fn vmm_list_marked_stop_for_update_by_sled_id(
&self,
opctx: &OpContext,
pagparams: &DataPageParams<'_, (Uuid, Uuid)>,
) -> ListResultVec<Vmm> {
paginated_multicolumn(dsl::vmm, (dsl::sled_id, dsl::id), pagparams)
Comment thread
hawkw marked this conversation as resolved.
Resolved
.filter(dsl::time_deleted.is_null())
.filter(dsl::stop_for_update_disposition_generation.is_not_null())
.filter(dsl::state.eq_any(DbVmmState::SHOULD_STOP_FOR_EVACUATION))
.select(Vmm::as_select())
.load_async(&*self.pool_connection_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
}
}

#[cfg(test)]
Expand All @@ -592,6 +613,7 @@ mod tests {
use omicron_uuid_kinds::InstanceUuid;
use omicron_uuid_kinds::SledUuid;
use sled_agent_types::instance::MigrationState;
use std::collections::BTreeSet;
use std::collections::HashMap;

#[tokio::test]
Expand Down Expand Up @@ -1325,6 +1347,107 @@ mod tests {
logctx.cleanup_successful();
}

#[tokio::test]
async fn test_vmm_list_marked_stop_for_update_by_sled_id() {
// Setup
let logctx =
dev::test_setup_log("test_vmm_list_marked_stop_for_update");
let db = TestDatabase::new_with_datastore(&logctx.log).await;
let (opctx, datastore) = (db.opctx(), db.datastore());
let conn = datastore.pool_connection_for_tests().await.unwrap();

// The states from which a VMM can still be stopped for an update. Only
// marked VMMs in these states should be listed by
// `vmm_list_marked_stop_for_update`.
let is_stoppable = |state: DbVmmState| {
matches!(
state,
DbVmmState::Creating
| DbVmmState::Starting
| DbVmmState::Running
| DbVmmState::Rebooting
)
};
Comment thread
hawkw marked this conversation as resolved.
Resolved

// Insert 2 VMMs (one marked to stop, one not) in each state per sled
// (2 sleds only).
let generation = UpdateDispositionGeneration::from(1);
let sled_a = SledUuid::new_v4();
let sled_b = SledUuid::new_v4();
let mut vmms = Vec::new();
for sled_id in [sled_a, sled_b] {
for &state in DbVmmState::ALL_STATES {
for marker in [Some(generation), None] {
let failure_reason = (state == DbVmmState::Failed)
.then_some(db::model::VmmFailureReason::FromSledAgent);
let vmm = datastore
.vmm_insert(
&opctx,
Vmm {
id: Uuid::new_v4(),
time_created: Utc::now(),
time_deleted: None,
instance_id: Uuid::new_v4(),
sled_id: sled_id.into(),
propolis_ip: "10.1.9.32".parse().unwrap(),
propolis_port: 420.into(),
cpu_platform: VmmCpuPlatform::SledDefault,
time_state_updated: Utc::now(),
generation: Generation::new(),
state,
failure_reason,
stop_for_update_disposition_generation: marker
.map(Into::into),
},
)
.await
.expect("VMM should be inserted successfully");
vmms.push(vmm);
}
}
}

// There should be 40 VMM rows. 2 VMMs (one marked, one not) per state
// per sled
let row_count: i64 = dsl::vmm
// Add a filter to ensure there is no full table scan
.filter(dsl::id.ne(Uuid::nil()))
.count()
.get_result_async(&*conn)
.await
.expect("VMMs should be counted");
assert_eq!(row_count, 40);

// Check there are 8 stoppable VMMs (4 per sled)
let expected_ids: BTreeSet<Uuid> = vmms
.iter()
.filter(|vmm| {
vmm.stop_for_update_disposition_generation.is_some()
&& is_stoppable(vmm.state)
})
.map(|vmm| vmm.id)
.collect();
assert_eq!(expected_ids.len(), 8);

// Check the retrieved ids for stoppable VMMs are the same as the
// expected ones
let listed = datastore
.vmm_list_marked_stop_for_update_by_sled_id(
&opctx,
&DataPageParams::max_page(),
)
.await
.expect("marked VMMs should be listed");
assert_eq!(listed.len(), expected_ids.len());
let listed_ids: BTreeSet<Uuid> =
listed.iter().map(|vmm| vmm.id).collect();
assert_eq!(listed_ids, expected_ids);

// Clean up.
db.terminate().await;
logctx.cleanup_successful();
}

async fn mark_failed_no_such_instance(
opctx: &OpContext,
datastore: &DataStore,
Expand Down
1 change: 1 addition & 0 deletions nexus/examples/config-second.toml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ instance_reincarnation.period_secs = 60
service_firewall_propagation.period_secs = 300
v2p_mapping_propagation.period_secs = 30
abandoned_vmm_reaper.period_secs = 60
vmm_stop_for_update.period_secs = 300
Comment thread
karencfv marked this conversation as resolved.
Resolved
saga_recovery.period_secs = 600
lookup_region_port.period_secs = 60
region_snapshot_replacement_start.period_secs = 30
Expand Down
1 change: 1 addition & 0 deletions nexus/examples/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ instance_reincarnation.period_secs = 60
service_firewall_propagation.period_secs = 300
v2p_mapping_propagation.period_secs = 30
abandoned_vmm_reaper.period_secs = 60
vmm_stop_for_update.period_secs = 300
Comment thread
karencfv marked this conversation as resolved.
Resolved
saga_recovery.period_secs = 600
lookup_region_port.period_secs = 60
region_snapshot_replacement_start.period_secs = 30
Expand Down
Loading