7e9ad8a78d
tracker-policy-pr / check-pr (pull_request) Successful in 13s
test / integration-docker (pull_request) Successful in 35s
test / unit (pull_request) Successful in 47s
lint / lint (push) Successful in 57s
test / integration-firecracker (pull_request) Successful in 3m35s
test / coverage (pull_request) Successful in 32s
test / publish-infra (pull_request) Has been skipped
Brings in main's 8 commits — #470 (backend-agnostic CI guards: BackendStatus enum, quiet status(), is_backend_available/is_backend_ready, tests/_backend.py skip_unless_backend) plus firecracker status() readiness checks (kvm/kernel/ dropbear/mke2fs). Reconciled #470's additions into the reorg'd backend structure: * BackendStatus enum + the status(*, quiet=) ABC signature -> backend/base.py * is_backend_available / is_backend_ready -> backend/selection.py * exposed all three through the lazy backend facade (__init__). Integration-test conflicts were our control_plane->orchestrator renames vs main's skip_unless_docker -> skip_unless_backend swap: kept our refactored import paths + main's guard. Repointed main's new is_backend_* tests to patch selection._backends (our moved home) instead of the facade. pyright: 0 errors. Full unit suite green (2272). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
128 lines
4.1 KiB
Python
128 lines
4.1 KiB
Python
"""FirecrackerBottleBackend — Linux microVM implementation.
|
|
|
|
Replaces smolmachines on Linux (issue #342): mature KVM-based
|
|
isolation via Firecracker, SSH control over a point-to-point TAP, and a
|
|
fail-closed nftables egress boundary. Selected by
|
|
`BOT_BOTTLE_BACKEND=firecracker` or `--backend=firecracker`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
from contextlib import contextmanager, redirect_stderr
|
|
from pathlib import Path
|
|
from typing import Generator, Sequence
|
|
|
|
from ...agent_provider import AgentProvisionPlan
|
|
from ...egress import EgressPlan
|
|
from ...env import ResolvedEnv
|
|
from ...git_gate import GitGatePlan
|
|
from ...manifest import Manifest
|
|
from ...supervisor.plan import SupervisePlan
|
|
from .. import ActiveAgent, BottleBackend, BottleImages, BottleSpec
|
|
from . import cleanup as _cleanup
|
|
from . import enumerate as _enumerate
|
|
from . import launch as _launch
|
|
from . import resolve_plan as _resolve_plan
|
|
from . import util as _util
|
|
from .bottle import FirecrackerBottle
|
|
from .bottle_cleanup_plan import FirecrackerBottleCleanupPlan
|
|
from .bottle_plan import FirecrackerBottlePlan
|
|
|
|
|
|
class FirecrackerBottleBackend(
|
|
BottleBackend["FirecrackerBottlePlan", "FirecrackerBottleCleanupPlan"]
|
|
):
|
|
name = "firecracker"
|
|
|
|
@classmethod
|
|
def is_available(cls) -> bool:
|
|
return _util.is_available()
|
|
|
|
@classmethod
|
|
def is_host_capable(cls) -> bool:
|
|
"""Linux + KVM, regardless of whether the `firecracker` binary
|
|
is installed. Drives default-backend selection so a capable host
|
|
without the binary still lands on firecracker and gets an
|
|
install pointer at launch."""
|
|
return _util.is_host_capable()
|
|
|
|
@classmethod
|
|
def setup(cls) -> int:
|
|
from . import setup as _setup
|
|
return _setup.setup()
|
|
|
|
@classmethod
|
|
def status(cls, *, quiet: bool = False) -> int:
|
|
from . import setup as _setup
|
|
if quiet:
|
|
with redirect_stderr(io.StringIO()):
|
|
return _setup.status()
|
|
return _setup.status()
|
|
|
|
@classmethod
|
|
def teardown(cls) -> int:
|
|
from . import setup as _setup
|
|
return _setup.teardown()
|
|
|
|
def _preflight(self) -> None:
|
|
_resolve_plan.preflight()
|
|
|
|
def _build_guest_env(self, resolved_env: ResolvedEnv) -> dict[str, str]:
|
|
return _resolve_plan.build_guest_env(resolved_env)
|
|
|
|
def _resolve_plan(
|
|
self,
|
|
spec: BottleSpec,
|
|
*,
|
|
manifest: Manifest,
|
|
slug: str,
|
|
resolved_env: ResolvedEnv,
|
|
agent_provision_plan: AgentProvisionPlan,
|
|
egress_plan: EgressPlan,
|
|
git_gate_plan: GitGatePlan,
|
|
supervise_plan: SupervisePlan | None,
|
|
stage_dir: Path,
|
|
) -> FirecrackerBottlePlan:
|
|
return _resolve_plan.resolve_plan(
|
|
spec,
|
|
manifest=manifest,
|
|
slug=slug,
|
|
resolved_env=resolved_env,
|
|
agent_provision_plan=agent_provision_plan,
|
|
egress_plan=egress_plan,
|
|
supervise_plan=supervise_plan,
|
|
git_gate_plan=git_gate_plan,
|
|
stage_dir=stage_dir,
|
|
)
|
|
|
|
def _build_or_load_images(self, plan: FirecrackerBottlePlan) -> BottleImages:
|
|
return BottleImages(agent=_launch.build_or_load_agent_base(plan))
|
|
|
|
def prelaunch_checks(self, plan: FirecrackerBottlePlan) -> None:
|
|
_launch.stale_checks(plan)
|
|
|
|
@contextmanager
|
|
def _launch_impl(
|
|
self, plan: FirecrackerBottlePlan, images: BottleImages,
|
|
) -> Generator[FirecrackerBottle, None, None]:
|
|
assert isinstance(images.agent, Path)
|
|
with _launch.launch(plan, images.agent, provision=self.provision) as bottle:
|
|
yield bottle
|
|
|
|
def prepare_cleanup(self) -> FirecrackerBottleCleanupPlan:
|
|
return _cleanup.prepare_cleanup()
|
|
|
|
def cleanup(self, plan: FirecrackerBottleCleanupPlan) -> None:
|
|
_cleanup.cleanup(plan)
|
|
|
|
def enumerate_active(self) -> Sequence[ActiveAgent]:
|
|
return _enumerate.enumerate_active()
|
|
|
|
def supervise_mcp_url(self, plan: FirecrackerBottlePlan) -> str:
|
|
return plan.agent_supervise_url
|
|
|
|
def ensure_orchestrator(self) -> str:
|
|
from . import infra_vm
|
|
return infra_vm.ensure_running().orchestrator_url
|