feat(firecracker): add Linux microVM backend to replace smolmachines
Adds a Firecracker-based backend for Linux, providing mature KVM-based microVM isolation to replace smolmachines/libkrun (issue #342, closes the dead-end tracked in #332). Architecture: - Guest control over SSH (dropbear injected into the rootfs) on a point-to-point TAP link. `ssh -t` forwards SIGWINCH natively, so no resize bridge is needed. - Networking: a one-time, root-provisioned pool of user-owned TAP devices (no shared bridge → no docker0/virbr0/cni0 collisions) plus a dedicated `table inet bot_bottle_fc` nftables table (independent of Docker/ufw/firewalld rules). `./cli.py firecracker setup` prints the host-appropriate config (NixOS module or sudo script). - Rootfs: `docker export` → ext4 via `mke2fs -d` (rootless, no mount), cached by image digest; per-bottle SSH pubkey + IP passed via the kernel cmdline. - Sidecar: reuses the Docker bundle, published on the slot's host TAP IP. - Fail-closed isolation: TAP pool verified at preflight; the egress boundary is proven empirically post-boot (before the agent runs) by a canary probe — the VM must fail to reach the host directly, or launch is refused. Linux hosts with Firecracker + KVM now default to this backend; macOS stays on macos-container. Not yet validated end-to-end on live hardware (requires the one-time network pool). Unit tests + pyright pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G8p32HJgPoS1hLPWubbftM
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"""Cleanup for the Firecracker backend.
|
||||
|
||||
Orphans are: firecracker VMM processes whose config lives under our run
|
||||
dir, the `bot-bottle-sidecars-*` containers, and the per-bottle run
|
||||
dirs. TAP slots free themselves (the flock drops when the launcher
|
||||
exits), so there is nothing to reclaim there.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from ...log import info
|
||||
from . import util
|
||||
from .bottle_cleanup_plan import FirecrackerBottleCleanupPlan
|
||||
|
||||
_SIDECAR_PREFIX = "bot-bottle-sidecars-"
|
||||
|
||||
|
||||
def _run_root() -> Path:
|
||||
return util.cache_dir() / "run"
|
||||
|
||||
|
||||
def _orphan_vm_pids() -> list[int]:
|
||||
"""firecracker processes whose --config-file is under our run dir."""
|
||||
run_root = str(_run_root())
|
||||
result = subprocess.run(
|
||||
["pgrep", "-a", "firecracker"],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return []
|
||||
pids: list[int] = []
|
||||
for line in result.stdout.splitlines():
|
||||
parts = line.split(None, 1)
|
||||
if len(parts) != 2 or run_root not in parts[1]:
|
||||
continue
|
||||
try:
|
||||
pids.append(int(parts[0]))
|
||||
except ValueError:
|
||||
continue
|
||||
return pids
|
||||
|
||||
|
||||
def _sidecar_containers() -> list[str]:
|
||||
result = subprocess.run(
|
||||
["docker", "ps", "-a", "--format", "{{.Names}}",
|
||||
"--filter", f"name={_SIDECAR_PREFIX}"],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return []
|
||||
return sorted(n.strip() for n in result.stdout.splitlines() if n.strip())
|
||||
|
||||
|
||||
def _run_dirs() -> list[str]:
|
||||
run_root = _run_root()
|
||||
if not run_root.is_dir():
|
||||
return []
|
||||
return sorted(str(p) for p in run_root.iterdir() if p.is_dir())
|
||||
|
||||
|
||||
def prepare_cleanup() -> FirecrackerBottleCleanupPlan:
|
||||
return FirecrackerBottleCleanupPlan(
|
||||
vm_pids=tuple(_orphan_vm_pids()),
|
||||
containers=tuple(_sidecar_containers()),
|
||||
run_dirs=tuple(_run_dirs()),
|
||||
)
|
||||
|
||||
|
||||
def cleanup(plan: FirecrackerBottleCleanupPlan) -> None:
|
||||
for pid in plan.vm_pids:
|
||||
info(f"kill firecracker VM pid {pid}")
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
for name in plan.containers:
|
||||
info(f"docker rm -f {name}")
|
||||
subprocess.run(
|
||||
["docker", "rm", "-f", name],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False,
|
||||
)
|
||||
for path in plan.run_dirs:
|
||||
info(f"rm -rf {path}")
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
Reference in New Issue
Block a user