Skip to content

Commit 5955256

Browse files
Add sandbox use-case snapshot E2E and scenario harness
1 parent b93e2a9 commit 5955256

6 files changed

Lines changed: 614 additions & 20 deletions

File tree

.beads/issues.jsonl

Lines changed: 10 additions & 0 deletions
Large diffs are not rendered by default.

crates/vz-linux/src/vm.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::path::Path;
12
use std::sync::Arc;
23
use std::time::Duration;
34

@@ -55,6 +56,37 @@ impl LinuxVm {
5556
Ok(())
5657
}
5758

59+
/// Save an in-place VM state snapshot and resume guest execution.
60+
///
61+
/// This pauses the VM, writes state to `path`, resumes the VM, and clears
62+
/// any cached gRPC client so subsequent operations reconnect cleanly.
63+
pub async fn save_state_snapshot(&self, path: &Path) -> Result<(), LinuxError> {
64+
self.vm.pause().await?;
65+
self.vm.save_state(path).await?;
66+
self.vm.resume().await?;
67+
let mut grpc = self.grpc.lock().await;
68+
*grpc = None;
69+
Ok(())
70+
}
71+
72+
/// Restore VM state from `path`, resume guest execution, and wait for agent.
73+
///
74+
/// This force-stops the current VM execution, restores state, resumes, and
75+
/// reestablishes guest-agent readiness before returning.
76+
pub async fn restore_state_snapshot(
77+
&self,
78+
path: &Path,
79+
agent_ready_timeout: Duration,
80+
) -> Result<(), LinuxError> {
81+
self.vm.stop().await?;
82+
self.vm.restore_state(path).await?;
83+
self.vm.resume().await?;
84+
let mut grpc = self.grpc.lock().await;
85+
*grpc = None;
86+
drop(grpc);
87+
self.wait_for_agent(agent_ready_timeout).await
88+
}
89+
5890
/// Start the VM and wait until guest agent is reachable.
5991
pub async fn start_and_wait_for_agent(
6092
&self,

crates/vz-oci-macos/src/runtime.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,6 +1163,67 @@ impl Runtime {
11631163
self.stack_vms.lock().await.contains_key(stack_id)
11641164
}
11651165

1166+
/// Save a shared stack VM snapshot to disk.
1167+
///
1168+
/// The VM is paused, state is saved, then the VM is resumed and the guest
1169+
/// agent is revalidated before returning.
1170+
pub async fn save_shared_vm_snapshot(
1171+
&self,
1172+
stack_id: &str,
1173+
state_path: impl AsRef<Path>,
1174+
) -> Result<(), OciError> {
1175+
let vm = self
1176+
.stack_vms
1177+
.lock()
1178+
.await
1179+
.get(stack_id)
1180+
.cloned()
1181+
.ok_or_else(|| {
1182+
OciError::InvalidConfig(format!("no shared VM running for stack '{stack_id}'"))
1183+
})?;
1184+
1185+
let state_path = state_path.as_ref();
1186+
if let Some(parent) = state_path.parent() {
1187+
fs::create_dir_all(parent)?;
1188+
}
1189+
1190+
vm.save_state_snapshot(state_path).await?;
1191+
vm.wait_for_agent(self.config.agent_ready_timeout).await?;
1192+
Ok(())
1193+
}
1194+
1195+
/// Restore a shared stack VM from a saved snapshot file.
1196+
///
1197+
/// Existing shared VM instance is stopped, restored from `state_path`, then
1198+
/// resumed and reconnected to the guest agent.
1199+
pub async fn restore_shared_vm_snapshot(
1200+
&self,
1201+
stack_id: &str,
1202+
state_path: impl AsRef<Path>,
1203+
) -> Result<(), OciError> {
1204+
let vm = self
1205+
.stack_vms
1206+
.lock()
1207+
.await
1208+
.get(stack_id)
1209+
.cloned()
1210+
.ok_or_else(|| {
1211+
OciError::InvalidConfig(format!("no shared VM running for stack '{stack_id}'"))
1212+
})?;
1213+
1214+
let state_path = state_path.as_ref();
1215+
if !state_path.exists() {
1216+
return Err(OciError::InvalidConfig(format!(
1217+
"shared VM snapshot path does not exist: {}",
1218+
state_path.display()
1219+
)));
1220+
}
1221+
1222+
vm.restore_state_snapshot(state_path, self.config.agent_ready_timeout)
1223+
.await?;
1224+
Ok(())
1225+
}
1226+
11661227
/// Execute a raw command in the shared VM (not through the OCI runtime).
11671228
///
11681229
/// Useful for diagnostics, inspecting the guest filesystem, or running

0 commit comments

Comments
 (0)