feat(firecracker): persistent registry volume for the infra VM (Stage B, 6/n)
lint / lint (push) Successful in 2m20s
test / unit (pull_request) Successful in 1m13s
test / integration (pull_request) Successful in 27s
test / coverage (pull_request) Successful in 1m24s

The infra VM's rootfs is ephemeral (rebuilt each boot), so the bottle
registry DB needs durable storage across restarts. Give the infra VM a
firecracker analogue of a docker volume: a host-side ext4 file attached as
a second virtio-block device (guest /dev/vdb), mounted at the control
plane's DB dir (/var/lib/bot-bottle, where host_db_path lives at
db/bot-bottle.db).

- firecracker_vm.boot/_config take an optional `data_drive` (a non-root,
  RW second drive).
- infra_vm creates the volume on first use (`mke2fs` an empty ext4 at
  <fc-cache>/infra/registry.ext4) and mounts /dev/vdb in the PID-1 init
  before the control plane starts. It's a plain ext4 file, so
  `sudo mount -o loop <path>` (VM stopped) inspects bot-bottle.db directly.

Verified on a KVM host: register a bottle, restart the infra VM (fresh
rootfs, same volume) — /dev/vdb re-mounts and the registry `db/` dir + a
marker file survive the restart.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
This commit is contained in:
2026-07-16 13:23:42 -04:00
parent fed99a2370
commit f6d27fd7b9
4 changed files with 97 additions and 8 deletions
@@ -78,20 +78,32 @@ def _config(
vcpus: int,
mem_mib: int,
guest_mac: str,
data_drive: Path | None = None,
) -> dict[str, object]:
drives: list[dict[str, object]] = [
{
"drive_id": "rootfs",
"path_on_host": str(rootfs),
"is_root_device": True,
"is_read_only": False,
}
]
# A second virtio-block device (guest /dev/vdb) — the infra VM's
# persistent registry "volume", a host-side ext4 file that outlives the
# ephemeral rootfs across VM restarts.
if data_drive is not None:
drives.append({
"drive_id": "data",
"path_on_host": str(data_drive),
"is_root_device": False,
"is_read_only": False,
})
return {
"boot-source": {
"kernel_image_path": str(util.kernel_path()),
"boot_args": _boot_args(guest_ip, host_ip, pubkey),
},
"drives": [
{
"drive_id": "rootfs",
"path_on_host": str(rootfs),
"is_root_device": True,
"is_read_only": False,
}
],
"drives": drives,
"network-interfaces": [
{
"iface_id": "eth0",
@@ -119,6 +131,7 @@ def boot(
mem_mib: int = 2048,
guest_mac: str = "06:00:AC:10:00:02",
detached: bool = False,
data_drive: Path | None = None,
) -> VmHandle:
"""Write the config and launch the VMM. Returns once the process is
spawned; callers wait for SSH readiness separately.
@@ -134,6 +147,7 @@ def boot(
_config(
rootfs=rootfs, tap=tap, guest_ip=guest_ip, host_ip=host_ip,
pubkey=pubkey, vcpus=vcpus, mem_mib=mem_mib, guest_mac=guest_mac,
data_drive=data_drive,
),
indent=2,
))
@@ -165,6 +165,7 @@ def boot() -> InfraVm:
name="bot-bottle-infra", rootfs=rootfs, tap=slot.iface,
guest_ip=slot.guest_ip, host_ip=slot.host_ip, pubkey=pubkey,
run_dir=run_dir, mem_mib=4096, detached=True,
data_drive=_ensure_registry_volume(),
)
_pid_file().write_text(str(vm.process.pid))
return InfraVm(guest_ip=slot.guest_ip, private_key=private_key, vm=vm)
@@ -180,6 +181,35 @@ def _pid_file() -> Path:
return _infra_dir() / "vm.pid"
# The registry "volume": a host-side ext4 file attached to the infra VM as a
# second virtio-block device (guest /dev/vdb), mounted at the control plane's
# DB dir. It outlives the ephemeral rootfs, so the bottle registry survives an
# infra-VM restart — the firecracker analogue of a docker volume. It is a
# plain ext4 file: `sudo mount -o loop <path>` on the host (with the VM
# stopped) to inspect bot-bottle.db directly.
_REGISTRY_SIZE = "512M"
def registry_volume_path() -> Path:
return _infra_dir() / "registry.ext4"
def _ensure_registry_volume() -> Path:
"""Create the empty ext4 registry volume on first use; reuse it after."""
vol = registry_volume_path()
if vol.exists():
return vol
info(f"creating infra registry volume {vol} ({_REGISTRY_SIZE})")
proc = subprocess.run(
["mke2fs", "-q", "-t", "ext4", "-F", str(vol), _REGISTRY_SIZE],
capture_output=True, text=True, check=False,
)
if proc.returncode != 0:
vol.unlink(missing_ok=True)
die(f"creating registry volume failed: {proc.stderr.strip()}")
return vol
def _stable_keypair() -> tuple[Path, str]:
"""The infra VM's SSH keypair — generated once and reused, so any later
launcher can SSH in (fetch CA / provision) even though a different process
@@ -321,6 +351,11 @@ if [ -n "$KEY" ]; then
fi
chown -R 0:0 /root 2>/dev/null || true
mkdir -p /etc/dropbear /run /var/lib/bot-bottle
# Persistent registry volume (second virtio-block device, /dev/vdb) mounted
# at the control plane's DB dir, so bot-bottle.db survives infra-VM restarts.
mount -t ext4 /dev/vdb /var/lib/bot-bottle 2>/dev/null || true
/bb-dropbear -R -E -p 22 &
# Control plane. Source is baked at /app; the package is stdlib-only.
+12
View File
@@ -334,6 +334,18 @@ class TestBootArgs(unittest.TestCase):
self.assertEqual("/run/rootfs.ext4", cfg["drives"][0]["path_on_host"])
self.assertFalse(cfg["drives"][0]["is_read_only"])
self.assertEqual("bbfc0", cfg["network-interfaces"][0]["host_dev_name"])
self.assertEqual(1, len(cfg["drives"])) # no data drive by default
def test_config_adds_data_drive(self):
cfg = cast(Any, firecracker_vm._config(
rootfs=Path("/run/rootfs.ext4"), tap="bbfc0",
guest_ip="100.64.0.1", host_ip="100.64.0.0", pubkey="k",
vcpus=2, mem_mib=2048, guest_mac="06:00:AC:10:00:02",
data_drive=Path("/run/registry.ext4"),
))
self.assertEqual(2, len(cfg["drives"]))
self.assertFalse(cfg["drives"][1]["is_root_device"])
self.assertEqual("/run/registry.ext4", cfg["drives"][1]["path_on_host"])
class TestBottleExecClose(unittest.TestCase):
+28
View File
@@ -38,6 +38,34 @@ class TestBuildInfraRootfs(unittest.TestCase):
self.assertIn("bot_bottle.orchestrator", init)
self.assertIn("gateway_init.py", init)
self.assertIn("export PATH=", init)
# Persistent registry volume mounted at the DB dir before the CP starts.
self.assertIn("/dev/vdb", init)
class TestRegistryVolume(unittest.TestCase):
def test_reuses_existing_volume(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
vol = Path(td) / "registry.ext4"
vol.write_bytes(b"") # already present
with patch.object(infra_vm, "registry_volume_path", return_value=vol), \
patch.object(infra_vm.subprocess, "run") as run:
out = infra_vm._ensure_registry_volume()
run.assert_not_called() # no mke2fs when it exists
self.assertEqual(vol, out)
def test_creates_volume_when_missing(self):
import tempfile
from subprocess import CompletedProcess
with tempfile.TemporaryDirectory() as td:
vol = Path(td) / "registry.ext4"
with patch.object(infra_vm, "registry_volume_path", return_value=vol), \
patch.object(infra_vm.subprocess, "run",
return_value=CompletedProcess([], 0)) as run:
infra_vm._ensure_registry_volume()
argv = run.call_args.args[0]
self.assertIn("mke2fs", argv)
self.assertIn(str(vol), argv)
class TestEnsureBuilt(unittest.TestCase):