use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use arcbox_vm_driver::net::{GuestNetwork, NetworkIdentity, NetworkLease};
use arcbox_vm_driver::{
CheckpointImage, IsolationSpec, NicSpec, PreparedVm, VmDriver, VmHandle, VmId,
};
use tracing::warn;
use crate::agent::{ClockSync, GuestAgent, GuestAgentFactory};
use crate::config::{JailerConfig, RuntimeConfig};
use crate::error::{ComputerError, Result};
use crate::lifecycle::runtime::ComputerRuntime;
use crate::sandbox;
use crate::sandbox::boot::{StageError, stage_rootfs_cow_or_copy};
use crate::sandbox::pool::PreparedSlot;
use crate::sandbox::reconcile::JournaledLease;
use crate::sandbox::spec::restore_spec;
use crate::snapshot::SnapshotMeta;
use crate::snapshot_cow::{CowHandle, CowManager};
pub struct RestoreVm<'a> {
pub new_id: &'a str,
pub snap_meta: &'a SnapshotMeta,
pub lease: Option<&'a NetworkLease>,
pub nic: Option<NicSpec>,
pub net_invariant: bool,
pub vm_dir: &'a Path,
pub jailer: &'a JailerConfig,
pub instance: &'a Arc<Mutex<ComputerRuntime>>,
pub claimed: Option<PreparedSlot>,
pub config: &'a RuntimeConfig,
pub cow_manager: &'a CowManager,
pub driver: &'a dyn VmDriver,
pub network: &'a dyn GuestNetwork,
pub agents: &'a dyn GuestAgentFactory,
}
pub struct RestoredVm {
pub prepared: Arc<dyn PreparedVm>,
pub handle: Arc<dyn VmHandle>,
pub agent: Arc<dyn GuestAgent>,
pub identity: Option<NetworkIdentity>,
pub cow_handle: Option<CowHandle>,
pub pool_hit: bool,
pub timings: RestoreTimings,
}
pub struct RestoreTimings {
pub prepared: Instant,
pub staged: Instant,
pub loaded: Instant,
pub guest_cfg: Instant,
}
pub struct RestoreFailure {
pub error: ComputerError,
pub prepared: Option<Arc<dyn PreparedVm>>,
pub cow_handle: Option<CowHandle>,
}
#[allow(
clippy::result_large_err,
reason = "the failure hands back the half-built sandbox's resources; boxing it is a refactor of its own"
)]
pub async fn restore_vm(inputs: RestoreVm<'_>) -> std::result::Result<RestoredVm, RestoreFailure> {
let RestoreVm {
new_id,
snap_meta,
lease,
nic,
net_invariant,
vm_dir,
jailer,
instance,
claimed,
config,
cow_manager,
driver,
network,
agents,
} = inputs;
let mut pending_cow: Option<CowHandle> = None;
let pool_hit = claimed.is_some();
let (prepared, resource_owner, rootfs, staged_checkpoint, t_prepared, t_staged) =
if let Some(slot) = claimed {
instance.lock().unwrap().pool_slot_id = Some(slot.slot_id.clone());
let handover = sandbox::reconcile::ComputerStateRecord::new(
new_id,
sandbox::journaled_pid(&*slot.prepared),
lease
.as_ref()
.map(|lease| JournaledLease::from_snapshot(lease, net_invariant)),
slot.cow_handle.as_ref(),
config,
None,
)
.map(|record| {
record
.with_vmm(sandbox::journaled_vmm(&*slot.prepared))
.with_pool_slot(Some(&slot.slot_id))
})
.and_then(|record| sandbox::reconcile::write_state_record(vm_dir, &record));
let PreparedSlot {
slot_id,
prepared: slot_prepared,
cow_handle,
rootfs,
image,
vm_dir: slot_vm_dir,
} = slot;
pending_cow = cow_handle;
match handover {
Ok(()) => {
if let Err(error) = sandbox::reconcile::clear_state_record(&slot_vm_dir) {
warn!(
sandbox_id = %new_id,
slot_id = %slot_id,
error = %error,
"claimed slot journal not cleared; the startup sweep will reconcile it"
);
} else if let Err(error) = tokio::fs::remove_dir_all(&slot_vm_dir).await
&& error.kind() != std::io::ErrorKind::NotFound
{
warn!(
sandbox_id = %new_id,
slot_id = %slot_id,
error = %error,
"claimed slot runtime dir not removed"
);
}
}
Err(error) => {
return Err(RestoreFailure {
error,
prepared: Some(slot_prepared),
cow_handle: pending_cow,
});
}
}
let t_claimed = std::time::Instant::now();
(slot_prepared, slot_id, rootfs, image, t_claimed, t_claimed)
} else {
let spawned: Result<Arc<dyn PreparedVm>> = async {
let prepared = sandbox::prepare_capability(driver)
.prepare(
&VmId::new(new_id)?,
&IsolationSpec::try_from(jailer)?,
vm_dir,
)
.await?;
Ok(Arc::from(prepared))
}
.await;
let spawned_prepared = match spawned {
Ok(spawned) => spawned,
Err(error) => {
return Err(RestoreFailure {
error,
prepared: None,
cow_handle: None,
});
}
};
let pid = sandbox::journaled_pid(&*spawned_prepared);
let vmm = sandbox::journaled_vmm(&*spawned_prepared);
let journal = |cow: Option<&CowHandle>| {
sandbox::reconcile::ComputerStateRecord::new(
new_id,
pid,
lease
.as_ref()
.map(|lease| JournaledLease::from_snapshot(lease, net_invariant)),
cow,
config,
None,
)
.map(|record| record.with_vmm(vmm.clone()))
.and_then(|record| sandbox::reconcile::write_state_record(vm_dir, &record))
};
if let Err(error) = journal(None) {
return Err(RestoreFailure {
error,
prepared: Some(Arc::clone(&spawned_prepared)),
cow_handle: None,
});
}
let t_prepared = std::time::Instant::now();
let setup_result: Result<(PathBuf, CheckpointImage)> = async {
let staging = sandbox::staging_capability(spawned_prepared.staging());
if let Some(k) = snap_meta.kernel_path.as_deref() {
staging.stage_kernel(Path::new(k)).await?;
}
let rootfs = snap_meta.rootfs_path.as_deref().ok_or_else(|| {
ComputerError::Snapshot(format!(
"checkpoint {} records no rootfs template; there is no disk to restore \
it onto",
snap_meta.id
))
})?;
let staged =
match stage_rootfs_cow_or_copy(cow_manager, staging, new_id, rootfs, &journal)
.await
{
Ok(staged) => staged,
Err(StageError { error, cow_handle }) => {
pending_cow = cow_handle;
return Err(error);
}
};
pending_cow = staged.cow_handle;
let image = staging
.stage_checkpoint(&sandbox::catalogued_checkpoint(snap_meta)?)
.await?;
Ok((staged.path, image))
}
.await;
let (rootfs, image) = match setup_result {
Ok(staged) => staged,
Err(error) => {
return Err(RestoreFailure {
error,
prepared: Some(Arc::clone(&spawned_prepared)),
cow_handle: pending_cow,
});
}
};
(
spawned_prepared,
new_id.to_owned(),
rootfs,
image,
t_prepared,
std::time::Instant::now(),
)
};
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 loaded: Result<(Arc<dyn VmHandle>, Arc<dyn GuestAgent>)> = async {
let restore = restore_spec(
&resource_owner,
rootfs,
nic.clone(),
IsolationSpec::try_from(jailer)?,
)?;
let handle: Arc<dyn VmHandle> =
Arc::from(prepared.restore(&staged_checkpoint, restore).await?);
let agent = agents.connect(Arc::clone(&handle), settled_on_load.as_ref())?;
Ok((handle, agent))
}
.await;
let (handle, agent) = match loaded {
Ok(loaded) => loaded,
Err(error) => {
return Err(RestoreFailure {
error,
prepared: Some(prepared),
cow_handle: pending_cow,
});
}
};
let t_loaded = std::time::Instant::now();
{
let id = new_id.to_owned();
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 restore");
}
Ok(Err(e)) => warn!(sandbox_id = %id, "clock sync after restore failed: {e}"),
Err(_) => warn!(sandbox_id = %id, "clock sync after restore timed out"),
}
});
}
let net_reconfig = async {
if snap_meta.net_invariant {
return Ok(());
}
let Some(identity) = identity.as_ref() else {
return Ok(());
};
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(|_| ComputerError::Vsock("net reconfig after restore timed out".into()))
.and_then(|r| r)
};
let configured = net_reconfig.await.and_then(|()| {
if snap_meta.net_invariant {
Ok(Arc::clone(&agent))
} else {
agents.connect(Arc::clone(&handle), identity.as_ref())
}
});
let agent = match configured {
Ok(agent) => agent,
Err(error) => {
return Err(RestoreFailure {
error,
prepared: Some(Arc::clone(&prepared)),
cow_handle: pending_cow,
});
}
};
let t_guest_cfg = std::time::Instant::now();
let adopted_slot = instance.lock().unwrap().pool_slot_id.clone();
let final_journal = sandbox::reconcile::ComputerStateRecord::new(
new_id,
sandbox::journaled_pid(&*prepared),
lease
.as_ref()
.map(|lease| JournaledLease::from_snapshot(lease, net_invariant)),
pending_cow.as_ref(),
config,
None,
)
.map(|record| {
record
.with_vmm(sandbox::journaled_vmm(&*prepared))
.with_pool_slot(adopted_slot.as_deref())
})
.and_then(|record| sandbox::reconcile::write_state_record(vm_dir, &record));
if let Err(error) = final_journal {
return Err(RestoreFailure {
error,
prepared: Some(Arc::clone(&prepared)),
cow_handle: pending_cow,
});
}
Ok(RestoredVm {
prepared,
handle,
agent,
identity,
cow_handle: pending_cow,
pool_hit,
timings: RestoreTimings {
prepared: t_prepared,
staged: t_staged,
loaded: t_loaded,
guest_cfg: t_guest_cfg,
},
})
}