Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion crates/openshell-driver-vm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Select the VM driver with `--compute-driver vm`, `OPENSHELL_COMPUTE_DRIVER=vm`,
| Configuration key | Default | Purpose |
|---|---|---|
| `grpc_endpoint` | empty | Required. URL the native host supervisor uses to reach the gateway. Host loopback such as `http://127.0.0.1:<port>` is valid. Legacy guest aliases are normalized to host loopback. This endpoint is never sent into the VM. |
| `state_dir` | `target/openshell-vm-driver` | Per-sandbox overlay disks, console logs, image cache, and private `run/compute-driver.sock` UDS. Relative paths are resolved to absolute paths at driver startup. |
| `state_dir` | OpenShell state directory under `vm-driver` (fallback: `target/openshell-vm-driver`) | Per-sandbox overlay disks, console logs, image cache, and private `run/compute-driver.sock` UDS. Relative paths are resolved to absolute paths at driver startup. |
| `driver_dir` | unset | Override the directory searched for `openshell-driver-vm`. |
| `default_image` | OpenShell base image | Sandbox image used when a create request omits one. |
| `bootstrap_image` | unset | VM runtime image used as the immutable bootstrap root disk. Falls back only to the operator-configured `default_image`; the driver refuses to start when both are empty. |
Expand Down Expand Up @@ -239,6 +239,40 @@ Image and writable-overlay preparation run in owned worker processes. Stopping o

At startup, the driver reclaims inactive attempts in this directory. It preserves active attempts, committed image caches, and unmarked staging from older releases. Concurrent preparations serialize image-cache publication; an interrupted attempt cannot publish a partial disk over an existing cache entry. Gateway upload slots for `rootfs_tar_path` keep their existing, separate lifecycle.

### Inspect and prune image disks

Run the VM driver binary on the gateway host. Pass the gateway's existing
`state_dir` explicitly; cache commands reject an omitted or missing state directory:

```shell
openshell-driver-vm --state-dir /path/to/vm-driver-state image-cache list
openshell-driver-vm --state-dir /path/to/vm-driver-state image-cache prune --unused --dry-run
```

`list` reports each committed root disk's cache ID, image reference, allocated
and apparent bytes, last-use Unix timestamp in milliseconds, cache layout
version, and persisted sandbox IDs that reference it. Older entries without
cache metadata show `-` for the image reference and last use. Disks prepared
from a rootfs tar show `-` for image reference because no container image
produced them. New cache uses record this metadata without changing the disk
contents.

Stop the VM gateway before removing entries, then run:

```shell
openshell-driver-vm --state-dir /path/to/vm-driver-state image-cache prune --unused
```

Prune removes only committed disks that no persisted sandbox references. It
leaves disks referenced by stopped sandboxes in place. For sandboxes created
before bootstrap identities were persisted, it conservatively preserves all
bootstrap disks. Prune refuses to run while a current driver holds the state
lock or an older driver is reachable at the default state socket. Stop older
standalone drivers that use a custom socket explicitly; their socket is outside
this check. The reported bytes are the disks' allocated bytes before removal.
Filesystem snapshots and shared blocks can make actual free space differ.
Nothing is removed automatically.

Each sandbox gets its own sparse writable
`<state-dir>/sandboxes/<id>/overlay.ext4`. Guest init mounts overlayfs as `/`
with the prepared image rootfs as lowerdir when present, otherwise the bootstrap
Expand Down
140 changes: 131 additions & 9 deletions crates/openshell-driver-vm/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
#[path = "preparation.rs"]
mod preparation;

#[path = "image_cache_admin.rs"]
pub mod image_cache_admin;

#[cfg(test)]
#[path = "registry_cache_tests.rs"]
mod registry_cache_tests;
Expand Down Expand Up @@ -1869,6 +1872,24 @@ impl VmDriver {
.await?
};
let image_identity = image_plan.image_identity.clone();
let bootstrap_image_ref = self.bootstrap_image_ref()?;
if let Err(error) = image_cache_admin::record_use(
&self.config.state_dir,
&image_plan.bootstrap_image_identity,
Some(bootstrap_image_ref.as_str()),
) {
warn!(%error, "vm driver: could not record bootstrap cache use");
}
if image_plan.image_disk.is_some()
&& let Err(error) = record_prepared_image_use(
&self.config.state_dir,
&image_identity,
&image_ref,
driver_config_had_rootfs_tar,
)
{
warn!(%error, "vm driver: could not record prepared image cache use");
}
self.ensure_provisioning_active(&sandbox.id).await?;
info!(
sandbox_id = %sandbox.id,
Expand Down Expand Up @@ -1918,8 +1939,13 @@ impl VmDriver {
.map_err(|err| Status::internal(format!("prepare guest overlay disk failed: {err}")))?;
self.ensure_provisioning_active(&sandbox.id).await?;

if let Err(err) =
write_sandbox_image_metadata(&state_dir, &image_ref, &image_identity).await
if let Err(err) = write_sandbox_image_metadata(
&state_dir,
&image_ref,
&image_identity,
&image_plan.bootstrap_image_identity,
)
.await
{
return Err(Status::internal(format!(
"write sandbox image metadata failed: {err}"
Expand Down Expand Up @@ -5197,6 +5223,11 @@ impl VmDriver {
#[doc(hidden)]
pub async fn run_image_preparation_worker(request_path: &Path) -> Result<(), String> {
let (request, directory) = preparation::read_request(request_path)?;
let _cache_lease = image_cache_admin::lock_cache(
&request.config.state_dir,
image_cache_admin::LockMode::Shared,
)
.map_err(|error| format!("lock image cache for preparation: {error}"))?;
validate_sandbox_id(&request.sandbox_id).map_err(|error| error.message().to_string())?;
preparation::watch_parent(request.parent_pid)?;
tracing_subscriber::fmt()
Expand Down Expand Up @@ -7333,22 +7364,57 @@ fn unique_image_cache_suffix() -> String {
format!("{}-{counter}", openshell_core::time::now_ms())
}

fn record_prepared_image_use(
state_dir: &Path,
image_identity: &str,
image_ref: &str,
from_rootfs_tar: bool,
) -> Result<(), std::io::Error> {
image_cache_admin::record_use(
state_dir,
image_identity,
(!from_rootfs_tar).then_some(image_ref),
)
}

async fn write_sandbox_identity_file(
state_dir: &Path,
filename: &str,
identity: &str,
) -> Result<(), std::io::Error> {
let sequence = IMAGE_CACHE_BUILD_COUNTER.fetch_add(1, Ordering::Relaxed);
let temporary = state_dir.join(format!(".{filename}.{}.{sequence}.tmp", std::process::id()));
let result = async {
write_private_file(&temporary, format!("{identity}\n").into_bytes()).await?;
tokio::fs::rename(&temporary, state_dir.join(filename)).await
}
.await;
if result.is_err() {
let _ = tokio::fs::remove_file(&temporary).await;
}
result
}

async fn write_sandbox_image_metadata(
state_dir: &Path,
image_ref: &str,
image_identity: &str,
bootstrap_image_identity: &str,
) -> Result<(), std::io::Error> {
tokio::fs::write(
state_dir.join(IMAGE_IDENTITY_FILE),
format!("{image_identity}\n"),
// Write the bootstrap reference first. If this fails, the old image
// identity still protects its disk when prune scans persisted state.
write_sandbox_identity_file(
state_dir,
image_cache_admin::BOOTSTRAP_IDENTITY_FILE,
bootstrap_image_identity,
)
.await?;
write_sandbox_identity_file(state_dir, IMAGE_IDENTITY_FILE, image_identity).await?;
tokio::fs::write(
state_dir.join(IMAGE_REFERENCE_FILE),
format!("{image_ref}\n"),
)
.await?;

Ok(())
}

Expand Down Expand Up @@ -9116,6 +9182,51 @@ mod tests {
}
}

#[tokio::test]
async fn failed_bootstrap_identity_write_preserves_prior_image_identity() {
let temp = tempfile::tempdir().unwrap();
let state_dir = temp.path();
tokio::fs::write(state_dir.join(IMAGE_IDENTITY_FILE), b"prior-image\n")
.await
.unwrap();
tokio::fs::create_dir(state_dir.join(image_cache_admin::BOOTSTRAP_IDENTITY_FILE))
.await
.unwrap();

assert!(
write_sandbox_image_metadata(
state_dir,
"new-image",
"new-image-id",
"new-bootstrap-id",
)
.await
.is_err()
);
assert_eq!(
tokio::fs::read_to_string(state_dir.join(IMAGE_IDENTITY_FILE))
.await
.unwrap(),
"prior-image\n"
);
}

#[test]
fn rootfs_tar_cache_use_has_no_image_reference() {
let temp = tempfile::tempdir().unwrap();
let cache_id = "sandbox-prepared-rootfs-ext4-umoci-v4-openshell-rootfs-tar";
let disk = image_cache_rootfs_image(temp.path(), cache_id);
fs::create_dir_all(disk.parent().unwrap()).unwrap();
fs::write(&disk, b"disk").unwrap();

record_prepared_image_use(temp.path(), cache_id, "unrelated/image:tag", true).unwrap();
let lease = image_cache_admin::lock_cache(temp.path(), image_cache_admin::LockMode::Shared)
.unwrap();
let entry = lease.list().unwrap().pop().unwrap();
assert_eq!(entry.image_ref, None);
assert!(entry.last_used_unix_ms.is_some());
}

async fn assert_inactive_restore_retains_identity(marker: &str, reason: &str) {
let temp = tempfile::tempdir().unwrap();
let mut driver = test_driver_with_extensions(LifecycleExtensionRegistry::new());
Expand Down Expand Up @@ -9149,9 +9260,20 @@ mod tests {
)
.await
.unwrap();
write_sandbox_image_metadata(&state_dir, "unused-image", "sha256:original-image")
.await
.unwrap();
write_sandbox_image_metadata(
&state_dir,
"unused-image",
"sha256:original-image",
"sha256:bootstrap-image",
)
.await
.unwrap();
assert_eq!(
tokio::fs::read_to_string(state_dir.join(image_cache_admin::BOOTSTRAP_IDENTITY_FILE))
.await
.unwrap(),
"sha256:bootstrap-image\n"
);
tokio::fs::write(state_dir.join(marker), b"inactive\n")
.await
.unwrap();
Expand Down
Loading
Loading