use std::path::Path;
use std::sync::Arc;
use arcbox_vm_driver::net::{GuestNetwork, NetworkLease};
use arcbox_vm_driver::{DiskSource, IsolationSpec, NicSpec, PreparedVm, VmDriver, VmHandle, VmId};
use tracing::warn;
use crate::agent::{ClockSync, GuestAgentFactory};
use crate::config::{JailerConfig, RuntimeConfig};
use crate::error::{Result, VmmError};
use crate::sandbox::pause::{PAUSED_ROOTFS_FILE, ResumeFailure, ResumedRuntime};
use crate::sandbox::reconcile::JournaledLease;
use crate::sandbox::spec::restore_spec;
use crate::sandbox::{self, ROOTFS_DISK_ID, SandboxId};
use crate::snapshot::SnapshotMeta;
use crate::snapshot_cow::{CowHandle, CowManager};
#[allow(
clippy::too_many_arguments,
reason = "the resume spans the resource set its computer owns"
)]
pub async fn restore_paused(
id: &SandboxId,
jailer: &JailerConfig,
snap_meta: &SnapshotMeta,
vm_dir: &Path,
networked: bool,
config: &RuntimeConfig,
cow_manager: &CowManager,
driver: &dyn VmDriver,
network: &dyn GuestNetwork,
agents: &dyn GuestAgentFactory,
) -> std::result::Result<ResumedRuntime, ResumeFailure> {
let mut lease: Option<NetworkLease> = None;
let mut nic: Option<NicSpec> = None;
let mut prepared: Option<Arc<dyn PreparedVm>> = None;
let mut cow_handle: Option<CowHandle> = None;
let attempt: Result<ResumedRuntime> = async {
if networked {
lease = Some(
network
.reserve(&VmId::new(id.as_str())?, sandbox::sandbox_network_policy())
.await?,
);
}
let journal = |vmm: Option<&Arc<dyn PreparedVm>>,
cow: Option<&CowHandle>,
net: Option<&NetworkLease>| {
let net =
net.map(|lease| JournaledLease::from_snapshot(lease, snap_meta.net_invariant));
sandbox::reconcile::SandboxStateRecord::new(
id,
vmm.and_then(|vmm| sandbox::journaled_pid(&**vmm)),
net,
cow,
config,
None,
)
.map(|record| record.with_vmm(vmm.and_then(|vmm| sandbox::journaled_vmm(&**vmm))))
.and_then(|record| sandbox::reconcile::write_state_record(vm_dir, &record))
};
journal(None, None, lease.as_ref())?;
if let Some(lease) = &lease {
nic = Some(
network
.activate(lease, sandbox::attach_mode(snap_meta.net_invariant))
.await?,
);
}
let spawned: Arc<dyn PreparedVm> = Arc::from(
sandbox::prepare_capability(driver)
.prepare(&VmId::new(id)?, &IsolationSpec::try_from(jailer)?, vm_dir)
.await?,
);
let staging = sandbox::staging_capability(spawned.staging());
prepared = Some(Arc::clone(&spawned));
journal(Some(&spawned), None, lease.as_ref())?;
if let Some(kernel) = snap_meta.kernel_path.as_deref() {
staging.stage_kernel(Path::new(kernel)).await?;
}
let preserved_cow = sandbox::preserved_cow_file(config, id);
let rootfs = if preserved_cow.exists() {
let template = snap_meta.rootfs_path.as_deref().ok_or_else(|| {
VmmError::Snapshot(format!(
"pause checkpoint for {id} records no rootfs template"
))
})?;
let handle = cow_manager.reattach(id, template).await?;
journal(prepared.as_ref(), Some(&handle), lease.as_ref())?;
let staged = staging
.stage_disk(
ROOTFS_DISK_ID,
DiskSource::Device(Path::new(&handle.dm_device)),
)
.await?;
cow_handle = Some(handle);
staged
} else {
let parked = vm_dir.join(PAUSED_ROOTFS_FILE);
if !parked.exists() {
return Err(VmmError::Snapshot(format!(
"paused sandbox {id} has neither a preserved overlay nor a parked rootfs"
)));
}
staging
.stage_disk(ROOTFS_DISK_ID, DiskSource::Handover(&parked))
.await?
};
let image = staging
.stage_checkpoint(&sandbox::catalogued_checkpoint(snap_meta)?)
.await?;
let restore = restore_spec(id, rootfs, nic.clone(), IsolationSpec::try_from(jailer)?)?;
let handle: Arc<dyn VmHandle> = Arc::from(
prepared
.as_ref()
.expect("prepared set above")
.restore(&image, restore)
.await?,
);
let identity = lease
.as_ref()
.map(|lease| network.identity(lease, sandbox::attach_mode(snap_meta.net_invariant)));
let settled_on_load = snap_meta.net_invariant.then(|| identity.clone()).flatten();
let agent = agents.connect(Arc::clone(&handle), settled_on_load.as_ref())?;
{
let id = id.clone();
let agent = Arc::clone(&agent);
tokio::spawn(async move {
match tokio::time::timeout(std::time::Duration::from_secs(10), agent.sync_clock())
.await
{
Ok(Ok(ClockSync::Synced)) => {}
Ok(Ok(ClockSync::AgentError(code))) => {
warn!(sandbox_id = %id, code, "agent could not set the clock after resume");
}
Ok(Err(e)) => warn!(sandbox_id = %id, "clock sync after resume failed: {e}"),
Err(_) => warn!(sandbox_id = %id, "clock sync after resume timed out"),
}
});
}
if let Some(identity) = &identity
&& !snap_meta.net_invariant
{
let cmd = crate::boot_proto::NetReconfigCommand {
ip: sandbox::ipv4(identity.ip)?,
netmask: sandbox::netmask(identity.prefix_len),
gateway: sandbox::ipv4(identity.gateway)?,
};
tokio::time::timeout(
std::time::Duration::from_secs(10),
agent.reconfigure_network(&cmd),
)
.await
.map_err(|_| VmmError::Vsock("net reconfig after resume timed out".into()))
.and_then(|r| r)?;
}
journal(prepared.as_ref(), cow_handle.as_ref(), lease.as_ref())?;
Ok(ResumedRuntime {
prepared: prepared.take().expect("prepared set above"),
handle,
net_identity: identity,
network: lease.take(),
cow_handle: cow_handle.take(),
})
}
.await;
match attempt {
Ok(resumed) => Ok(resumed),
Err(error) => {
let unwound = unwind_resume(
id,
vm_dir,
config,
cow_manager,
network,
prepared,
cow_handle,
lease,
)
.await;
Err(ResumeFailure { error, unwound })
}
}
}
async fn park_copy_mode_rootfs(
id: &SandboxId,
vm_dir: &Path,
config: &RuntimeConfig,
prepared: Option<&Arc<dyn PreparedVm>>,
) -> bool {
let Some(prepared) = prepared else {
return true;
};
if sandbox::preserved_cow_file(config, id).exists() {
return true;
}
match sandbox::staging_capability(prepared.staging())
.unstage_disk(ROOTFS_DISK_ID, &vm_dir.join(PAUSED_ROOTFS_FILE))
.await
{
Ok(_) => true,
Err(error) => {
warn!(sandbox_id = %id, error = %error, "resume unwind: parking rootfs failed");
false
}
}
}
#[allow(
clippy::too_many_arguments,
reason = "the unwind spans everything the failed resume re-created"
)]
async fn unwind_resume(
id: &SandboxId,
vm_dir: &Path,
config: &RuntimeConfig,
cow_manager: &CowManager,
network: &dyn GuestNetwork,
prepared: Option<Arc<dyn PreparedVm>>,
cow_handle: Option<CowHandle>,
net_lease: Option<NetworkLease>,
) -> bool {
let mut clean = park_copy_mode_rootfs(id, vm_dir, config, prepared.as_ref()).await;
if let Some(prepared) = prepared {
if let Err(error) = prepared.discard().await {
warn!(sandbox_id = %id, error = %error, "resume unwind: the vmm did not exit");
clean = false;
}
}
if let Some(handle) = cow_handle
&& let Err(error) = cow_manager.detach_keep_cow(&handle).await
{
warn!(sandbox_id = %id, error = %error, "resume unwind: overlay detach failed");
clean = false;
}
if let Some(lease) = net_lease
&& let Err(error) = network.quarantine(lease).await
{
warn!(sandbox_id = %id, error = %error, "resume unwind: network quarantine failed");
clean = false;
}
if clean && let Err(error) = sandbox::reconcile::clear_state_record(vm_dir) {
warn!(sandbox_id = %id, error = %error, "resume unwind: journal removal failed");
clean = false;
}
clean
}