feat(firecracker): run the orchestrator control plane as a VM (Stage B, 1/n)
First slice of Stage B: the orchestrator control plane runs as a
persistent Firecracker infra VM instead of a Docker container. The host
CLI reaches it over HTTP at the orchestrator link's guest IP; agent VMs
will reach its gateway ports (added next) over VM-to-VM routing.
- Dockerfile.orchestrator bakes the stdlib-only control-plane source
(COPY bot_bottle) so the image is self-contained and runs from a built
image with no runtime bind-mount — a guest VM can't bind-mount host
source. (Build-from-source stays the default; a pull-from-registry mode
lands later. The docker backend's dev bind-mount still overlays this.)
- util.build_base_rootfs_dir / inject_guest_boot take a `variant` +
`init_script`, so the same orchestrator image is prepared two ways
without a cache collision: the builder VM keeps the SSH-only agent init;
the infra VM gets a control-plane PID-1 init.
- new firecracker/infra_vm.py: boot the infra VM on the orchestrator link,
run `python -m bot_bottle.orchestrator` as PID 1, and poll /health.
Verified on a KVM host: infra VM boots, control plane answers
`GET /health -> 200 {"status":"ok"}` from the host over the TAP link.
Next: fold gateway_init (egress/git-gate/supervise) into the same VM,
then agent->gateway routing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
This commit is contained in:
@@ -159,15 +159,22 @@ def docker_image_id(ref: str) -> str:
|
||||
return result.stdout.strip().replace("sha256:", "")[:16]
|
||||
|
||||
|
||||
def build_base_rootfs_dir(image_ref: str) -> Path:
|
||||
"""Export the agent image's filesystem and inject the guest init +
|
||||
static dropbear. Cached by image digest — the per-bottle bits
|
||||
def build_base_rootfs_dir(
|
||||
image_ref: str, *, variant: str = "", init_script: str | None = None,
|
||||
) -> Path:
|
||||
"""Export the image's filesystem and inject the guest init + static
|
||||
dropbear. Cached by image digest — the per-bottle bits
|
||||
(authorized_keys, IP) are passed at boot via the kernel cmdline, so
|
||||
this tree carries nothing bottle-specific and is safely shared.
|
||||
|
||||
`variant` suffixes the cache key so the same image can be prepared
|
||||
with a different `init_script` (e.g. the infra VM boots the same
|
||||
orchestrator image as the builder but runs the control plane as
|
||||
PID 1, not the SSH-only agent init) without a cache collision.
|
||||
|
||||
Returns the prepared directory (read as the `mke2fs -d` source)."""
|
||||
digest = docker_image_id(image_ref)
|
||||
base = cache_dir() / "rootfs" / digest
|
||||
base = cache_dir() / "rootfs" / f"{digest}{variant}"
|
||||
ready = base / ".bb-ready"
|
||||
if ready.is_file():
|
||||
return base
|
||||
@@ -200,17 +207,19 @@ def build_base_rootfs_dir(image_ref: str) -> Path:
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False,
|
||||
)
|
||||
|
||||
inject_guest_boot(base)
|
||||
inject_guest_boot(base, init_script=init_script)
|
||||
ready.write_text("ok\n")
|
||||
return base
|
||||
|
||||
|
||||
def inject_guest_boot(rootfs: Path) -> None:
|
||||
"""Drop the static dropbear and the PID-1 init into the rootfs."""
|
||||
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
|
||||
passes its own (control plane + gateway) init."""
|
||||
shutil.copy2(dropbear_path(), rootfs / "bb-dropbear")
|
||||
os.chmod(rootfs / "bb-dropbear", 0o755)
|
||||
init = rootfs / "bb-init"
|
||||
init.write_text(_GUEST_INIT)
|
||||
init.write_text(init_script or _GUEST_INIT)
|
||||
os.chmod(init, 0o755)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user