247 lines
8.9 KiB
Python
247 lines
8.9 KiB
Python
"""Embedded runtime resolver and diagnostics."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import importlib.resources as resources
|
|
import json
|
|
import os
|
|
import stat
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from pyro_mcp.vm_network import TapNetworkManager
|
|
|
|
DEFAULT_PLATFORM = "linux-x86_64"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RuntimePaths:
|
|
"""Resolved paths for bundled runtime components."""
|
|
|
|
bundle_root: Path
|
|
manifest_path: Path
|
|
firecracker_bin: Path
|
|
jailer_bin: Path
|
|
guest_agent_path: Path | None
|
|
artifacts_dir: Path
|
|
notice_path: Path
|
|
manifest: dict[str, Any]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RuntimeCapabilities:
|
|
"""Feature flags inferred from the bundled runtime."""
|
|
|
|
supports_vm_boot: bool
|
|
supports_guest_exec: bool
|
|
supports_guest_network: bool
|
|
reason: str | None = None
|
|
|
|
|
|
def _sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as fp:
|
|
for block in iter(lambda: fp.read(1024 * 1024), b""):
|
|
digest.update(block)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def _ensure_executable(path: Path) -> None:
|
|
mode = path.stat().st_mode
|
|
if mode & stat.S_IXUSR:
|
|
return
|
|
path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
|
|
|
|
def _default_bundle_parent() -> Path:
|
|
return Path(str(resources.files("pyro_mcp.runtime_bundle")))
|
|
|
|
|
|
def resolve_runtime_paths(
|
|
*,
|
|
platform: str = DEFAULT_PLATFORM,
|
|
verify_checksums: bool = True,
|
|
) -> RuntimePaths:
|
|
"""Resolve and validate embedded runtime assets."""
|
|
bundle_parent = Path(os.environ.get("PYRO_RUNTIME_BUNDLE_DIR", _default_bundle_parent()))
|
|
bundle_root = bundle_parent / platform
|
|
manifest_path = bundle_root / "manifest.json"
|
|
notice_path = bundle_parent / "NOTICE"
|
|
|
|
if not manifest_path.exists():
|
|
raise RuntimeError(
|
|
f"bundled runtime manifest not found at {manifest_path}; reinstall package or "
|
|
"use a wheel that includes bundled runtime assets"
|
|
)
|
|
if not notice_path.exists():
|
|
raise RuntimeError(f"runtime NOTICE file missing at {notice_path}")
|
|
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
if not isinstance(manifest, dict):
|
|
raise RuntimeError("invalid runtime manifest format")
|
|
|
|
binaries = manifest.get("binaries")
|
|
if not isinstance(binaries, dict):
|
|
raise RuntimeError("runtime manifest is missing `binaries`")
|
|
firecracker_entry = binaries.get("firecracker")
|
|
jailer_entry = binaries.get("jailer")
|
|
if not isinstance(firecracker_entry, dict) or not isinstance(jailer_entry, dict):
|
|
raise RuntimeError("runtime manifest does not define firecracker/jailer binaries")
|
|
|
|
firecracker_bin = bundle_root / str(firecracker_entry.get("path", ""))
|
|
jailer_bin = bundle_root / str(jailer_entry.get("path", ""))
|
|
guest_agent_path: Path | None = None
|
|
guest = manifest.get("guest")
|
|
if isinstance(guest, dict):
|
|
agent_entry = guest.get("agent")
|
|
if isinstance(agent_entry, dict):
|
|
raw_agent_path = agent_entry.get("path")
|
|
if isinstance(raw_agent_path, str):
|
|
guest_agent_path = bundle_root / raw_agent_path
|
|
artifacts_dir = bundle_root / "profiles"
|
|
|
|
required_paths = [firecracker_bin, jailer_bin]
|
|
if guest_agent_path is not None:
|
|
required_paths.append(guest_agent_path)
|
|
|
|
for path in required_paths:
|
|
if not path.exists():
|
|
raise RuntimeError(f"runtime asset missing: {path}")
|
|
|
|
_ensure_executable(firecracker_bin)
|
|
_ensure_executable(jailer_bin)
|
|
|
|
if verify_checksums:
|
|
for entry in (firecracker_entry, jailer_entry):
|
|
raw_path = entry.get("path")
|
|
raw_hash = entry.get("sha256")
|
|
if not isinstance(raw_path, str) or not isinstance(raw_hash, str):
|
|
raise RuntimeError("runtime binary manifest entry is malformed")
|
|
full_path = bundle_root / raw_path
|
|
actual = _sha256(full_path)
|
|
if actual != raw_hash:
|
|
raise RuntimeError(
|
|
f"runtime checksum mismatch for {full_path}; expected {raw_hash}, got {actual}"
|
|
)
|
|
if isinstance(guest, dict):
|
|
agent_entry = guest.get("agent")
|
|
if isinstance(agent_entry, dict):
|
|
raw_path = agent_entry.get("path")
|
|
raw_hash = agent_entry.get("sha256")
|
|
if not isinstance(raw_path, str) or not isinstance(raw_hash, str):
|
|
raise RuntimeError("runtime guest agent manifest entry is malformed")
|
|
full_path = bundle_root / raw_path
|
|
actual = _sha256(full_path)
|
|
if actual != raw_hash:
|
|
raise RuntimeError(
|
|
f"runtime checksum mismatch for {full_path}; "
|
|
f"expected {raw_hash}, got {actual}"
|
|
)
|
|
return RuntimePaths(
|
|
bundle_root=bundle_root,
|
|
manifest_path=manifest_path,
|
|
firecracker_bin=firecracker_bin,
|
|
jailer_bin=jailer_bin,
|
|
guest_agent_path=guest_agent_path,
|
|
artifacts_dir=artifacts_dir,
|
|
notice_path=notice_path,
|
|
manifest=manifest,
|
|
)
|
|
|
|
|
|
def runtime_capabilities(paths: RuntimePaths) -> RuntimeCapabilities:
|
|
"""Infer what the current bundled runtime can actually do."""
|
|
binary_text = paths.firecracker_bin.read_text(encoding="utf-8", errors="ignore")
|
|
if "bundled firecracker shim" in binary_text:
|
|
return RuntimeCapabilities(
|
|
supports_vm_boot=False,
|
|
supports_guest_exec=False,
|
|
supports_guest_network=False,
|
|
reason="bundled runtime uses shim firecracker/jailer binaries",
|
|
)
|
|
|
|
capabilities = paths.manifest.get("capabilities")
|
|
if not isinstance(capabilities, dict):
|
|
return RuntimeCapabilities(
|
|
supports_vm_boot=False,
|
|
supports_guest_exec=False,
|
|
supports_guest_network=False,
|
|
reason="runtime manifest does not declare guest boot/exec/network capabilities",
|
|
)
|
|
|
|
supports_vm_boot = bool(capabilities.get("vm_boot"))
|
|
supports_guest_exec = bool(capabilities.get("guest_exec"))
|
|
supports_guest_network = bool(capabilities.get("guest_network"))
|
|
reason = None
|
|
if not supports_vm_boot:
|
|
reason = "runtime manifest does not advertise real VM boot support"
|
|
return RuntimeCapabilities(
|
|
supports_vm_boot=supports_vm_boot,
|
|
supports_guest_exec=supports_guest_exec,
|
|
supports_guest_network=supports_guest_network,
|
|
reason=reason,
|
|
)
|
|
|
|
|
|
def doctor_report(*, platform: str = DEFAULT_PLATFORM) -> dict[str, Any]:
|
|
"""Build a runtime diagnostics report."""
|
|
report: dict[str, Any] = {
|
|
"platform": platform,
|
|
"runtime_ok": False,
|
|
"issues": [],
|
|
"kvm": {
|
|
"exists": Path("/dev/kvm").exists(),
|
|
"readable": os.access("/dev/kvm", os.R_OK),
|
|
"writable": os.access("/dev/kvm", os.W_OK),
|
|
},
|
|
"networking": {
|
|
"enabled_by_default": TapNetworkManager().enabled,
|
|
},
|
|
}
|
|
network = TapNetworkManager.diagnostics()
|
|
report["networking"].update(
|
|
{
|
|
"tun_available": network.tun_available,
|
|
"ip_binary": network.ip_binary,
|
|
"nft_binary": network.nft_binary,
|
|
"iptables_binary": network.iptables_binary,
|
|
"ip_forward_enabled": network.ip_forward_enabled,
|
|
}
|
|
)
|
|
try:
|
|
paths = resolve_runtime_paths(platform=platform, verify_checksums=True)
|
|
except Exception as exc: # noqa: BLE001
|
|
report["issues"] = [str(exc)]
|
|
return report
|
|
|
|
capabilities = runtime_capabilities(paths)
|
|
from pyro_mcp.vm_environments import EnvironmentStore
|
|
|
|
environment_store = EnvironmentStore(runtime_paths=paths)
|
|
report["runtime_ok"] = True
|
|
report["runtime"] = {
|
|
"bundle_root": str(paths.bundle_root),
|
|
"manifest_path": str(paths.manifest_path),
|
|
"firecracker_bin": str(paths.firecracker_bin),
|
|
"jailer_bin": str(paths.jailer_bin),
|
|
"guest_agent_path": str(paths.guest_agent_path) if paths.guest_agent_path else None,
|
|
"artifacts_dir": str(paths.artifacts_dir),
|
|
"artifacts_present": paths.artifacts_dir.exists(),
|
|
"notice_path": str(paths.notice_path),
|
|
"bundle_version": paths.manifest.get("bundle_version"),
|
|
"component_versions": paths.manifest.get("component_versions", {}),
|
|
"capabilities": {
|
|
"supports_vm_boot": capabilities.supports_vm_boot,
|
|
"supports_guest_exec": capabilities.supports_guest_exec,
|
|
"supports_guest_network": capabilities.supports_guest_network,
|
|
"reason": capabilities.reason,
|
|
},
|
|
"catalog_version": environment_store.catalog_version,
|
|
"cache_dir": str(environment_store.cache_dir),
|
|
"environments": environment_store.list_environments(),
|
|
}
|
|
if not report["kvm"]["exists"]:
|
|
report["issues"] = ["/dev/kvm is not available on this host"]
|
|
return report
|