use std::sync::{Arc, Mutex};
use arcbox_vm_driver::net::GuestNetwork;
use crate::config::RuntimeConfig;
use crate::error::{ComputerError, Result};
use crate::lifecycle::runtime::ComputerRuntime;
use crate::sandbox::pause::PAUSED_ROOTFS_FILE;
use crate::sandbox::{self, ComputerId, ROOTFS_DISK_ID};
use crate::snapshot_cow::CowManager;
pub async fn release_for_pause(
id: &ComputerId,
arc: &Arc<Mutex<ComputerRuntime>>,
config: &RuntimeConfig,
cow_manager: &CowManager,
network: &dyn GuestNetwork,
) -> Result<()> {
let (prepared, handle, in_copy_mode, vm_dir) = {
let computer = arc.lock().unwrap();
(
computer.prepared.clone(),
computer.handle.clone(),
computer.cow_handle.is_none(),
computer.vm_dir.clone(),
)
};
if in_copy_mode {
let staging = prepared
.as_deref()
.map(|prepared| sandbox::staging_capability(prepared.staging()))
.or_else(|| {
handle
.as_deref()
.map(|handle| sandbox::staging_capability(handle.staging()))
});
let parked = match staging {
Some(staging) => {
staging
.unstage_disk(ROOTFS_DISK_ID, &vm_dir.join(PAUSED_ROOTFS_FILE))
.await?
}
None => false,
};
if !parked {
return Err(ComputerError::Snapshot(format!(
"computer {id} runs on a copied rootfs but nothing could be taken out of its \
vm's area; pausing it would leave nothing to resume from"
)));
}
}
super::release::kill_computer_process(id, arc).await?;
let owner = super::release::resource_owner(id, arc);
let cow_handle = arc.lock().unwrap().cow_handle.take();
if let Some(handle) = cow_handle {
if let Err(error) = cow_manager.detach_keep_cow(&handle).await {
arc.lock().unwrap().cow_handle = Some(handle);
return Err(error.into());
}
let retained = sandbox::preserved_cow_file(config, id);
let detached = sandbox::preserved_cow_file(config, &owner);
if detached != retained && detached.exists() {
tokio::fs::rename(&detached, &retained)
.await
.map_err(ComputerError::Io)?;
}
}
{
let lease = arc.lock().unwrap().network.take();
if let Some(lease) = lease
&& let Err(error) = network.quarantine(lease.clone()).await
{
arc.lock().unwrap().network = Some(lease);
return Err(error.into());
}
}
arc.lock().unwrap().pool_slot_id = None;
Ok(())
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use arcbox_vm_driver::testkit::{FakeDriver, FakeNetwork};
use arcbox_vm_driver::{
BootSpec, ConsoleSpec, DiskSource, IsolationSpec, PreparedVm, ShutdownMode, VmDriver as _,
VmId, VmSpec,
};
use super::*;
use crate::sandbox::ComputerSpec;
use crate::snapshot_cow::CowOptions;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Grip {
Prepared,
AdoptedHandle,
}
async fn copy_mode_computer(
data_dir: &std::path::Path,
driver: &FakeDriver,
staged: Option<&[u8]>,
grip: Grip,
) -> (Arc<Mutex<ComputerRuntime>>, Arc<dyn PreparedVm>, PathBuf) {
let vm_dir = data_dir.join("sandboxes/job");
std::fs::create_dir_all(&vm_dir).unwrap();
let id = VmId::new("job").unwrap();
let prepared: Arc<dyn PreparedVm> = Arc::from(
driver
.prepare()
.unwrap()
.prepare(&id, &IsolationSpec::None, &vm_dir)
.await
.unwrap(),
);
if let Some(bytes) = staged {
let source = data_dir.join("template.ext4");
std::fs::write(&source, bytes).unwrap();
prepared
.staging()
.unwrap()
.stage_disk(ROOTFS_DISK_ID, DiskSource::Image(&source))
.await
.unwrap();
}
let mut computer = ComputerRuntime::new(
"job".to_owned(),
ComputerSpec::default(),
None,
vm_dir.clone(),
);
match grip {
Grip::Prepared => computer.prepared = Some(Arc::clone(&prepared)),
Grip::AdoptedHandle => {
computer.handle = Some(Arc::from(prepared.boot(vm_spec(&id)).await.unwrap()));
}
}
(Arc::new(Mutex::new(computer)), prepared, vm_dir)
}
fn vm_spec(id: &VmId) -> VmSpec {
VmSpec {
id: id.clone(),
cpus: 1,
memory_mib: 128,
boot: BootSpec::Kernel {
image: "/vmlinux".into(),
cmdline: String::new(),
initrd: None,
},
disks: vec![],
nics: vec![],
vsock: None,
shares: vec![],
console: ConsoleSpec::Off,
balloon: false,
entropy: false,
dirty_tracking: false,
isolation: IsolationSpec::None,
}
}
fn config(data_dir: &std::path::Path) -> RuntimeConfig {
let mut config = RuntimeConfig::default();
config.firecracker.data_dir = data_dir.to_string_lossy().into_owned();
config
}
#[tokio::test]
async fn the_copy_mode_disk_is_parked_before_the_vm_is_discarded() {
let data_dir = tempfile::tempdir().unwrap();
let driver = FakeDriver::new();
let (computer, prepared, vm_dir) = copy_mode_computer(
data_dir.path(),
&driver,
Some(b"the guest's disk"),
Grip::Prepared,
)
.await;
let config = config(data_dir.path());
let cow = CowManager::new(CowOptions::new(&config.firecracker.data_dir)).unwrap();
release_for_pause(
&"job".to_owned(),
&computer,
&config,
&cow,
&FakeNetwork::new(),
)
.await
.expect("a copy-mode pause parks the disk and releases the vm");
assert_eq!(
std::fs::read(vm_dir.join(PAUSED_ROOTFS_FILE)).unwrap(),
b"the guest's disk"
);
assert!(
!prepared.alive(),
"the vm is discarded after the disk is out"
);
}
#[tokio::test]
async fn a_copy_mode_pause_with_no_staged_disk_is_refused_before_the_kill() {
let data_dir = tempfile::tempdir().unwrap();
let driver = FakeDriver::new();
let (computer, prepared, vm_dir) =
copy_mode_computer(data_dir.path(), &driver, None, Grip::Prepared).await;
let config = config(data_dir.path());
let cow = CowManager::new(CowOptions::new(&config.firecracker.data_dir)).unwrap();
let refused = release_for_pause(
&"job".to_owned(),
&computer,
&config,
&cow,
&FakeNetwork::new(),
)
.await;
match refused {
Err(ComputerError::Snapshot(message)) => {
assert!(message.contains("nothing to resume from"), "{message}");
}
other => panic!("expected a refusal, got {other:?}"),
}
assert!(prepared.alive(), "the vm is left running for a retry");
assert!(!vm_dir.join(PAUSED_ROOTFS_FILE).exists());
}
#[tokio::test]
async fn an_adopted_copy_mode_computer_pauses_through_its_handle() {
let data_dir = tempfile::tempdir().unwrap();
let driver = FakeDriver::new();
let (computer, _prepared, vm_dir) = copy_mode_computer(
data_dir.path(),
&driver,
Some(b"the guest's disk"),
Grip::AdoptedHandle,
)
.await;
assert!(
computer.lock().unwrap().prepared.is_none(),
"an adopted computer has only its handle"
);
let config = config(data_dir.path());
let cow = CowManager::new(CowOptions::new(&config.firecracker.data_dir)).unwrap();
release_for_pause(
&"job".to_owned(),
&computer,
&config,
&cow,
&FakeNetwork::new(),
)
.await
.expect("an adopted copy-mode computer pauses");
assert_eq!(
std::fs::read(vm_dir.join(PAUSED_ROOTFS_FILE)).unwrap(),
b"the guest's disk"
);
assert_eq!(
driver.shutdowns(&VmId::new("job").unwrap()),
[ShutdownMode::Kill]
);
}
}