1614172423
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
62 lines
2.3 KiB
Python
62 lines
2.3 KiB
Python
"""Unit tests for the Firecracker infra VM (control-plane VM boot).
|
|
|
|
The KVM boot / HTTP reachability is integration-tested on a KVM host; here
|
|
we cover the URL shape, the rootfs-variant wiring, and the health-poll
|
|
decisions that must hold without a VM.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from bot_bottle.backend.firecracker import infra_vm
|
|
|
|
|
|
class TestControlPlaneUrl(unittest.TestCase):
|
|
def test_url_uses_guest_ip_and_port(self):
|
|
infra = infra_vm.InfraVm(
|
|
vm=MagicMock(), guest_ip="10.243.255.1", private_key=Path("/k"))
|
|
self.assertEqual(
|
|
f"http://10.243.255.1:{infra_vm.CONTROL_PLANE_PORT}",
|
|
infra.control_plane_url,
|
|
)
|
|
|
|
|
|
class TestBuildInfraRootfs(unittest.TestCase):
|
|
def test_uses_infra_variant_and_init(self):
|
|
with patch.object(infra_vm.util, "build_base_rootfs_dir") as build:
|
|
build.return_value = Path("/cache/rootfs/x-infra")
|
|
infra_vm.build_infra_rootfs_dir()
|
|
build.assert_called_once()
|
|
self.assertEqual(infra_vm._ORCHESTRATOR_IMAGE, build.call_args.args[0])
|
|
self.assertEqual("-infra", build.call_args.kwargs["variant"])
|
|
# The init runs the control plane, not the SSH-only agent init.
|
|
self.assertIn("bot_bottle.orchestrator", build.call_args.kwargs["init_script"])
|
|
|
|
|
|
class TestWaitForHealth(unittest.TestCase):
|
|
def _infra(self, alive: bool = True) -> infra_vm.InfraVm:
|
|
vm = MagicMock()
|
|
vm.is_alive.return_value = alive
|
|
return infra_vm.InfraVm(vm=vm, guest_ip="10.0.0.1", private_key=Path("/k"))
|
|
|
|
def test_returns_on_200(self):
|
|
infra = self._infra()
|
|
cm = MagicMock()
|
|
cm.__enter__.return_value.status = 200
|
|
with patch.object(infra_vm.urllib.request, "urlopen", return_value=cm):
|
|
infra_vm.wait_for_health(infra, timeout=5) # must not raise
|
|
|
|
def test_dies_when_vm_exits(self):
|
|
infra = self._infra(alive=False)
|
|
infra.vm.process.returncode = 1
|
|
with patch.object(infra_vm.firecracker_vm, "_console_tail", return_value=""), \
|
|
self.assertRaises(SystemExit):
|
|
infra_vm.wait_for_health(infra, timeout=5)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|