Skip to content

Commit df0b451

Browse files
committed
server: allow VCR replacement during block backend start
Modify the state driver's VM startup procedure to allow the driver to process Crucible volume configuration changes while block backends are being activated. This fixes a livelock that occurs when starting a VM with a Crucible VCR that points to an unavailable downstairs: the unavailable downstairs prevents Crucible activation from proceeding; Nexus sends a corrected VCR that, if applied, would allow the upstairs to activate; but the state driver never applies the new VCR because it's blocked trying to activate using the broken VCR. Modify the PHD VCR replacement smoke test so that it checks this behavior. Add an affordance to PHD Crucible disks that allows a test to specify that the disk's generated VCRs should contain an invalid downstairs IP. Start a VM with a disk configured this way, then replace the broken VCR with a corrected VCR and verify that the VM boots normally.
1 parent 42b8735 commit df0b451

File tree

9 files changed

+323
-109
lines changed

9 files changed

+323
-109
lines changed

bin/propolis-server/src/lib/vm/objects.rs

+19-54
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
2323

2424
use crate::{serial::Serial, spec::Spec, vcpu_tasks::VcpuTaskController};
2525

26-
use super::{
27-
state_driver::VmStartReason, BlockBackendMap, CrucibleBackendMap, DeviceMap,
28-
};
26+
use super::{BlockBackendMap, CrucibleBackendMap, DeviceMap};
2927

3028
/// A collection of components that make up a Propolis VM instance.
3129
pub(crate) struct VmObjects {
@@ -189,6 +187,14 @@ impl VmObjectsLocked {
189187
&self.ps2ctrl
190188
}
191189

190+
pub(crate) fn device_map(&self) -> &DeviceMap {
191+
&self.devices
192+
}
193+
194+
pub(crate) fn block_backend_map(&self) -> &BlockBackendMap {
195+
&self.block_backends
196+
}
197+
192198
/// Iterates over all of the lifecycle trait objects in this VM and calls
193199
/// `func` on each one.
194200
pub(crate) fn for_each_device(
@@ -244,33 +250,6 @@ impl VmObjectsLocked {
244250
self.machine.reinitialize().unwrap();
245251
}
246252

247-
/// Starts a VM's devices and allows all of its vCPU tasks to run.
248-
///
249-
/// This function may be called either after initializing a new VM from
250-
/// scratch or after an inbound live migration. In the latter case, this
251-
/// routine assumes that the caller initialized and activated the VM's vCPUs
252-
/// prior to importing state from the migration source.
253-
pub(super) async fn start(
254-
&mut self,
255-
reason: VmStartReason,
256-
) -> anyhow::Result<()> {
257-
match reason {
258-
VmStartReason::ExplicitRequest => {
259-
self.reset_vcpus();
260-
}
261-
VmStartReason::MigratedIn => {
262-
self.resume_kernel_vm();
263-
}
264-
}
265-
266-
let result = self.start_devices().await;
267-
if result.is_ok() {
268-
self.vcpu_tasks.resume_all();
269-
}
270-
271-
result
272-
}
273-
274253
/// Pauses this VM's devices and its kernel VMM.
275254
pub(crate) async fn pause(&mut self) {
276255
// Order matters here: the Propolis lifecycle trait's pause function
@@ -291,6 +270,16 @@ impl VmObjectsLocked {
291270
self.vcpu_tasks.resume_all();
292271
}
293272

273+
/// Resumes this VM's vCPU tasks.
274+
///
275+
/// This is intended for use in VM startup sequences where the state driver
276+
/// needs fine-grained control over the order in which devices and vCPUs
277+
/// start. When pausing and resuming a VM that's already been started, use
278+
/// [`Self::pause`] and [`Self::resume`] instead.
279+
pub(crate) async fn resume_vcpus(&mut self) {
280+
self.vcpu_tasks.resume_all();
281+
}
282+
294283
/// Stops the VM's vCPU tasks and devices.
295284
pub(super) async fn halt(&mut self) {
296285
self.vcpu_tasks.exit_all();
@@ -324,30 +313,6 @@ impl VmObjectsLocked {
324313
self.vcpu_tasks.resume_all();
325314
}
326315

327-
/// Starts all of a VM's devices and allows its block backends to process
328-
/// requests from their devices.
329-
async fn start_devices(&self) -> anyhow::Result<()> {
330-
self.for_each_device_fallible(|name, dev| {
331-
info!(self.log, "sending startup complete to {}", name);
332-
let res = dev.start();
333-
if let Err(e) = &res {
334-
error!(self.log, "startup failed for {}: {:?}", name, e);
335-
}
336-
res
337-
})?;
338-
339-
for (name, backend) in self.block_backends.iter() {
340-
info!(self.log, "starting block backend {}", name);
341-
let res = backend.start().await;
342-
if let Err(e) = &res {
343-
error!(self.log, "startup failed for {}: {:?}", name, e);
344-
return res;
345-
}
346-
}
347-
348-
Ok(())
349-
}
350-
351316
/// Pauses all of a VM's devices.
352317
async fn pause_devices(&self) {
353318
// Take care not to wedge the runtime with any device pause

bin/propolis-server/src/lib/vm/request_queue.rs

+22-11
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,7 @@ impl ExternalRequestQueue {
210210
reboot: RequestDisposition::Deny(
211211
RequestDeniedReason::InstanceNotActive,
212212
),
213-
mutate: RequestDisposition::Deny(
214-
RequestDeniedReason::InstanceNotActive,
215-
),
213+
mutate: RequestDisposition::Enqueue,
216214
stop: RequestDisposition::Enqueue,
217215
},
218216
log,
@@ -295,7 +293,7 @@ impl ExternalRequestQueue {
295293
start: Disposition::Ignore,
296294
migrate_as_source: Disposition::Deny(reason),
297295
reboot: Disposition::Deny(reason),
298-
mutate: Disposition::Deny(reason),
296+
mutate: Disposition::Enqueue,
299297
stop: self.allowed.stop,
300298
}
301299
}
@@ -577,18 +575,27 @@ mod test {
577575
}
578576

579577
#[tokio::test]
580-
async fn mutation_requires_running_and_not_migrating_out() {
578+
async fn mutation_requires_not_migrating_out() {
581579
let mut queue =
582580
ExternalRequestQueue::new(test_logger(), InstanceAutoStart::No);
583581

584-
// Mutating a VM before it has started is not allowed.
585-
assert!(queue.try_queue(make_reconfigure_crucible_request()).is_err());
582+
// Mutating a VM before it has started is allowed.
583+
assert!(queue.try_queue(make_reconfigure_crucible_request()).is_ok());
584+
assert!(matches!(
585+
queue.pop_front(),
586+
Some(ExternalRequest::ReconfigureCrucibleVolume { .. })
587+
));
586588

587-
// Merely dequeuing the start request doesn't allow mutation; the VM
588-
// actually has to be running.
589+
// Mutating a VM is also allowed while it is starting.
589590
assert!(queue.try_queue(ExternalRequest::Start).is_ok());
590591
assert!(matches!(queue.pop_front(), Some(ExternalRequest::Start)));
591-
assert!(queue.try_queue(make_reconfigure_crucible_request()).is_err());
592+
assert!(queue.try_queue(make_reconfigure_crucible_request()).is_ok());
593+
assert!(matches!(
594+
queue.pop_front(),
595+
Some(ExternalRequest::ReconfigureCrucibleVolume { .. })
596+
));
597+
598+
// And it's allowed once the VM has started running.
592599
queue.notify_instance_state_change(InstanceStateChange::StartedRunning);
593600
assert!(queue.try_queue(make_reconfigure_crucible_request()).is_ok());
594601
assert!(matches!(
@@ -610,10 +617,14 @@ mod test {
610617
}
611618

612619
#[tokio::test]
613-
async fn mutation_disallowed_after_stop() {
620+
async fn mutation_disallowed_after_stop_requested() {
614621
let mut queue =
615622
ExternalRequestQueue::new(test_logger(), InstanceAutoStart::Yes);
616623
queue.notify_instance_state_change(InstanceStateChange::StartedRunning);
624+
625+
assert!(queue.try_queue(ExternalRequest::Stop).is_ok());
626+
assert!(queue.try_queue(make_reconfigure_crucible_request()).is_err());
627+
617628
queue.notify_instance_state_change(InstanceStateChange::Stopped);
618629
assert!(queue.try_queue(make_reconfigure_crucible_request()).is_err());
619630
}

0 commit comments

Comments
 (0)