Add real runtime materialization pipeline and bundle artifacts

This commit is contained in:
Thales Maciel 2026-03-06 19:26:29 -03:00
parent cbf212bb7b
commit c43c718c83
32 changed files with 1456 additions and 27 deletions

View file

@ -12,6 +12,8 @@ def test_resolve_runtime_paths_default_bundle() -> None:
paths = resolve_runtime_paths()
assert paths.firecracker_bin.exists()
assert paths.jailer_bin.exists()
assert paths.guest_agent_path is not None
assert paths.guest_agent_path.exists()
assert (paths.artifacts_dir / "debian-git" / "vmlinux").exists()
assert paths.manifest.get("platform") == "linux-x86_64"
@ -45,8 +47,14 @@ def test_resolve_runtime_paths_checksum_mismatch(
firecracker_path = copied_platform / "bin" / "firecracker"
firecracker_path.parent.mkdir(parents=True, exist_ok=True)
firecracker_path.write_text("tampered\n", encoding="utf-8")
(copied_platform / "bin" / "jailer").write_text(
(source.jailer_bin).read_text(encoding="utf-8"),
(copied_platform / "bin" / "jailer").write_bytes(source.jailer_bin.read_bytes())
guest_agent_path = source.guest_agent_path
if guest_agent_path is None:
raise AssertionError("expected guest agent in runtime bundle")
copied_guest_dir = copied_platform / "guest"
copied_guest_dir.mkdir(parents=True, exist_ok=True)
(copied_guest_dir / "pyro_guest_agent.py").write_text(
guest_agent_path.read_text(encoding="utf-8"),
encoding="utf-8",
)
for profile in ("debian-base", "debian-git", "debian-build"):
@ -54,9 +62,7 @@ def test_resolve_runtime_paths_checksum_mismatch(
profile_dir.mkdir(parents=True, exist_ok=True)
for filename in ("vmlinux", "rootfs.ext4"):
source_file = source.artifacts_dir / profile / filename
(profile_dir / filename).write_text(
source_file.read_text(encoding="utf-8"), encoding="utf-8"
)
(profile_dir / filename).write_bytes(source_file.read_bytes())
monkeypatch.setenv("PYRO_RUNTIME_BUNDLE_DIR", str(copied_bundle))
with pytest.raises(RuntimeError, match="checksum mismatch"):
@ -72,6 +78,8 @@ def test_doctor_report_has_runtime_fields() -> None:
runtime = report.get("runtime")
assert isinstance(runtime, dict)
assert "firecracker_bin" in runtime
assert "guest_agent_path" in runtime
assert "component_versions" in runtime
networking = report["networking"]
assert isinstance(networking, dict)
assert "tun_available" in networking

189
tests/test_runtime_build.py Normal file
View file

@ -0,0 +1,189 @@
from __future__ import annotations
import hashlib
import json
import tarfile
from pathlib import Path
import pytest
from pyro_mcp.runtime_build import (
_build_paths,
_load_lock,
build_bundle,
generate_manifest,
materialize_binaries,
stage_agent,
stage_binaries,
stage_kernel,
stage_rootfs,
validate_sources,
)
def _write_text(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
def _make_source_tree(tmp_path: Path) -> tuple[Path, Path, Path]:
source_dir = tmp_path / "runtime_sources"
platform_root = source_dir / "linux-x86_64"
_write_text(source_dir / "NOTICE", "notice\n")
_write_text(platform_root / "bin/firecracker", "firecracker\n")
_write_text(platform_root / "bin/jailer", "jailer\n")
_write_text(platform_root / "guest/pyro_guest_agent.py", "#!/usr/bin/env python3\n")
_write_text(platform_root / "profiles/debian-base/vmlinux", "kernel-base\n")
_write_text(platform_root / "profiles/debian-base/rootfs.ext4", "rootfs-base\n")
_write_text(platform_root / "profiles/debian-git/vmlinux", "kernel-git\n")
_write_text(platform_root / "profiles/debian-git/rootfs.ext4", "rootfs-git\n")
_write_text(platform_root / "profiles/debian-build/vmlinux", "kernel-build\n")
_write_text(platform_root / "profiles/debian-build/rootfs.ext4", "rootfs-build\n")
lock = {
"bundle_version": "9.9.9",
"platform": "linux-x86_64",
"component_versions": {
"firecracker": "1.0.0",
"jailer": "1.0.0",
"kernel": "6.0.0",
"guest_agent": "0.2.0",
"base_distro": "debian-12",
},
"capabilities": {"vm_boot": True, "guest_exec": True, "guest_network": True},
"binaries": {"firecracker": "bin/firecracker", "jailer": "bin/jailer"},
"guest": {"agent": {"path": "guest/pyro_guest_agent.py"}},
"profiles": {
"debian-base": {
"description": "base",
"kernel": "profiles/debian-base/vmlinux",
"rootfs": "profiles/debian-base/rootfs.ext4",
},
"debian-git": {
"description": "git",
"kernel": "profiles/debian-git/vmlinux",
"rootfs": "profiles/debian-git/rootfs.ext4",
},
"debian-build": {
"description": "build",
"kernel": "profiles/debian-build/vmlinux",
"rootfs": "profiles/debian-build/rootfs.ext4",
},
},
}
(platform_root / "runtime.lock.json").write_text(
json.dumps(lock, indent=2) + "\n", encoding="utf-8"
)
build_dir = tmp_path / "build/runtime_bundle"
bundle_dir = tmp_path / "bundle_out"
return source_dir, build_dir, bundle_dir
def test_runtime_build_stages_and_manifest(tmp_path: Path) -> None:
source_dir, build_dir, bundle_dir = _make_source_tree(tmp_path)
paths = _build_paths(
source_dir=source_dir,
build_dir=build_dir,
bundle_dir=bundle_dir,
materialized_dir=tmp_path / "materialized_sources",
platform="linux-x86_64",
)
lock = _load_lock(paths)
paths.build_platform_root.mkdir(parents=True, exist_ok=True)
stage_binaries(paths, lock)
stage_kernel(paths, lock)
stage_rootfs(paths, lock)
stage_agent(paths, lock)
manifest = generate_manifest(paths, lock)
assert manifest["bundle_version"] == "9.9.9"
assert manifest["capabilities"]["guest_exec"] is True
assert manifest["component_versions"]["guest_agent"] == "0.2.0"
assert (paths.build_platform_root / "guest/pyro_guest_agent.py").exists()
def test_runtime_build_bundle_syncs_output(tmp_path: Path) -> None:
source_dir, build_dir, bundle_dir = _make_source_tree(tmp_path)
paths = _build_paths(
source_dir=source_dir,
build_dir=build_dir,
bundle_dir=bundle_dir,
materialized_dir=tmp_path / "materialized_sources",
platform="linux-x86_64",
)
manifest = build_bundle(paths, sync=True)
assert manifest["profiles"]["debian-git"]["description"] == "git"
assert (bundle_dir / "NOTICE").exists()
assert (bundle_dir / "linux-x86_64/manifest.json").exists()
assert (bundle_dir / "linux-x86_64/guest/pyro_guest_agent.py").exists()
def test_runtime_build_rejects_guest_capabilities_for_placeholder_sources(tmp_path: Path) -> None:
source_dir, build_dir, bundle_dir = _make_source_tree(tmp_path)
paths = _build_paths(
source_dir=source_dir,
build_dir=build_dir,
bundle_dir=bundle_dir,
materialized_dir=tmp_path / "materialized_sources",
platform="linux-x86_64",
)
lock_path = source_dir / "linux-x86_64/runtime.lock.json"
_write_text(
source_dir / "linux-x86_64/bin/firecracker",
"#!/usr/bin/env bash\n"
"echo 'bundled firecracker shim'\n",
)
_write_text(
source_dir / "linux-x86_64/profiles/debian-base/rootfs.ext4",
"placeholder-rootfs\n",
)
lock = json.loads(lock_path.read_text(encoding="utf-8"))
lock["capabilities"] = {"vm_boot": True, "guest_exec": True, "guest_network": True}
lock_path.write_text(json.dumps(lock, indent=2) + "\n", encoding="utf-8")
with pytest.raises(RuntimeError, match="guest-capable features"):
validate_sources(paths, _load_lock(paths))
def test_runtime_build_materializes_firecracker_release(tmp_path: Path) -> None:
source_dir, build_dir, bundle_dir = _make_source_tree(tmp_path)
archive_path = tmp_path / "firecracker-v1.12.1-x86_64.tgz"
release_dir = "release-v1.12.1-x86_64"
with tarfile.open(archive_path, "w:gz") as archive:
firecracker_path = tmp_path / "firecracker-bin"
jailer_path = tmp_path / "jailer-bin"
firecracker_path.write_text("real-firecracker\n", encoding="utf-8")
jailer_path.write_text("real-jailer\n", encoding="utf-8")
archive.add(firecracker_path, arcname=f"{release_dir}/firecracker-v1.12.1-x86_64")
archive.add(jailer_path, arcname=f"{release_dir}/jailer-v1.12.1-x86_64")
digest = hashlib.sha256(archive_path.read_bytes()).hexdigest()
lock_path = source_dir / "linux-x86_64/runtime.lock.json"
lock = json.loads(lock_path.read_text(encoding="utf-8"))
lock["upstream"] = {
"firecracker_release": {
"archive_url": archive_path.resolve().as_uri(),
"archive_sha256": digest,
"firecracker_member": f"{release_dir}/firecracker-v1.12.1-x86_64",
"jailer_member": f"{release_dir}/jailer-v1.12.1-x86_64",
}
}
lock_path.write_text(json.dumps(lock, indent=2) + "\n", encoding="utf-8")
paths = _build_paths(
source_dir=source_dir,
build_dir=build_dir,
bundle_dir=bundle_dir,
materialized_dir=tmp_path / "materialized_sources",
platform="linux-x86_64",
)
materialize_binaries(paths, _load_lock(paths))
assert (paths.materialized_platform_root / "bin/firecracker").read_text(encoding="utf-8") == (
"real-firecracker\n"
)
assert (paths.materialized_platform_root / "bin/jailer").read_text(encoding="utf-8") == (
"real-jailer\n"
)

View file

@ -47,5 +47,8 @@ def test_build_launch_plan_writes_expected_files(tmp_path: Path) -> None:
rendered = json.loads(plan.config_path.read_text(encoding="utf-8"))
assert rendered["machine-config"]["vcpu_count"] == 2
assert rendered["network-interfaces"][0]["host_dev_name"] == "pyroabcdef12"
assert "init=/opt/pyro/bin/pyro-init" in rendered["boot-source"]["boot_args"]
assert "pyro.guest_ip=172.29.100.2" in rendered["boot-source"]["boot_args"]
assert "pyro.gateway_ip=172.29.100.1" in rendered["boot-source"]["boot_args"]
guest_exec = json.loads(plan.guest_exec_path.read_text(encoding="utf-8"))
assert guest_exec["transport"] == "vsock"