feat(firecracker): port freeze/migrate off host Docker (PRD 0069 / #397)
The last host-Docker dependency in the Firecracker launch path. Freeze and resume no longer touch the docker daemon, so the backend needs firecracker + KVM only — completing #348. Freeze: stream the guest rootfs over SSH straight into a persistent committed-rootfs.tar (the resumable/migratable artifact) instead of round-tripping through `docker build` from a scratch image. Written to a .partial sibling and atomically renamed so a failed freeze leaves no truncated artifact. Resume: extract the snapshot tar into a cached base dir and feed it to the existing rootless `mke2fs -d` pipeline, replacing the `docker create` + `docker export | tar` path. Recreate the proc/sys/dev/run mount points the freezer excludes so the guest init can mount them. `util.build_base_rootfs_dir` / `docker_image_id` stay — they still back the opt-in BOT_BOTTLE_INFRA_BUILD=local dev path and off-host publish_infra, which are out of scope. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ
This commit is contained in:
@@ -14,6 +14,7 @@ and `./cli.py backend setup --backend=firecracker`.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
@@ -212,6 +213,49 @@ def build_base_rootfs_dir(
|
||||
return base
|
||||
|
||||
|
||||
def build_committed_rootfs_dir(tar_path: Path) -> Path:
|
||||
"""Prepare a base rootfs dir from a frozen-bottle snapshot tar (the
|
||||
freeze/resume path — no Docker). Extracts the snapshot, recreates the
|
||||
virtual mount points the freezer excluded, and injects the guest init +
|
||||
static dropbear, mirroring `build_base_rootfs_dir` but sourced from a tar
|
||||
we control rather than a Docker image.
|
||||
|
||||
Cached under the rootfs cache, keyed by the tar's size+mtime so a
|
||||
re-freeze re-extracts but repeated resumes of the same snapshot don't.
|
||||
Returns the prepared directory (read as the `mke2fs -d` source)."""
|
||||
st = tar_path.stat()
|
||||
fingerprint = hashlib.sha256(
|
||||
f"{tar_path}:{st.st_size}:{st.st_mtime_ns}".encode()
|
||||
).hexdigest()[:16]
|
||||
base = cache_dir() / "rootfs" / f"committed-{fingerprint}"
|
||||
ready = base / ".bb-ready"
|
||||
if ready.is_file():
|
||||
return base
|
||||
|
||||
if base.exists():
|
||||
shutil.rmtree(base, ignore_errors=True)
|
||||
base.mkdir(parents=True)
|
||||
|
||||
info(f"extracting committed rootfs {tar_path} -> {base}")
|
||||
result = subprocess.run(
|
||||
["tar", "-x", "-f", str(tar_path), "-C", str(base)],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
die(f"extracting committed rootfs {tar_path} failed: "
|
||||
f"{result.stderr.strip() or '<no stderr>'}")
|
||||
|
||||
# The freezer excludes the live/virtual filesystems from the snapshot;
|
||||
# recreate them as empty mount points so the guest init can mount
|
||||
# proc/sys/dev and dropbear has a writable /run.
|
||||
for mount_point in ("proc", "sys", "dev", "run"):
|
||||
(base / mount_point).mkdir(mode=0o755, exist_ok=True)
|
||||
|
||||
inject_guest_boot(base)
|
||||
ready.write_text("ok\n")
|
||||
return base
|
||||
|
||||
|
||||
def inject_guest_boot(rootfs: Path, init_script: str | None = None) -> None:
|
||||
"""Drop the static dropbear and the PID-1 init into the rootfs.
|
||||
`init_script` defaults to the SSH-only agent init; the infra VM
|
||||
|
||||
Reference in New Issue
Block a user