mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 05:51:57 +00:00
78f24dfe3f
- microvm_sandbox:Linux 上 available() 要求 /dev/kvm 存在且可读写,不可用时给出具体原因(没装、运行时没装、没有 KVM、没有权限) - 新增 app.sandbox.selftest:真起一台 VM 执行一行代码,部署脚本用它判断 - Dockerfile:WITH_MICROVM(默认 1)构建期 install 并 pull python:3.12-slim,自带的 msb、libkrunfw 改为链接,镜像约多 150 MB - deploy.sh:ENABLE_MICROVM=auto|0|1,有 KVM 时用临时容器试启动,成功才加 --device /dev/kvm 和所属组;部署完和 status 打印实际用的沙箱 - package-docker.sh:--no-microvm;部署说明、配置模板、README 同步
152 lines
6.7 KiB
Python
152 lines
6.7 KiB
Python
"""Linux 上 microVM 没有 KVM 就起不来,可用性判断要把这一条算进去。
|
||
|
||
以前 available() 只看运行时装没装。运行时和沙箱镜像打进 Docker 镜像以后,没有 KVM 的机器上
|
||
(Mac 上的 Docker Desktop、没开嵌套虚拟化的云主机)auto 会选中 microVM,每次执行代码都启动失败。
|
||
selftest 是部署脚本用的:真起一台 VM,起得来才把 /dev/kvm 交给容器。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from app.sandbox import manager as mgr
|
||
from app.sandbox import microvm_sandbox as mv
|
||
from app.sandbox import selftest
|
||
from app.sandbox.base import ExecResult
|
||
from app.sandbox.local_sandbox import LocalSandbox
|
||
|
||
|
||
@pytest.fixture
|
||
def linux(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
|
||
"""在 Linux 上、运行时已装好、镜像已缓存;返回假的 /dev/kvm 路径,默认不存在。"""
|
||
kvm = tmp_path / "kvm"
|
||
monkeypatch.setattr(mv, "_on_linux", lambda: True)
|
||
monkeypatch.setattr(mv, "_KVM_DEVICE", kvm)
|
||
monkeypatch.setattr(mv.MicroVMSandbox, "_runtime_installed", staticmethod(lambda: True))
|
||
monkeypatch.setattr(mv.MicroVMSandbox, "image_ready", staticmethod(lambda: True))
|
||
return kvm
|
||
|
||
|
||
def test_no_kvm_device_means_unavailable(linux: Path) -> None:
|
||
assert mv.MicroVMSandbox.available() is False
|
||
reason = mv.MicroVMSandbox.unavailable_reason()
|
||
assert "/dev/kvm" in reason and "挂进容器" in reason
|
||
|
||
|
||
def test_kvm_without_permission_means_unavailable(linux: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||
linux.touch()
|
||
monkeypatch.setattr(mv.os, "access", lambda path, mode: False)
|
||
assert mv.MicroVMSandbox.available() is False
|
||
assert "kvm 组" in mv.MicroVMSandbox.unavailable_reason()
|
||
|
||
|
||
def test_kvm_present_and_writable(linux: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||
linux.touch()
|
||
monkeypatch.setattr(mv.os, "access", lambda path, mode: True)
|
||
assert mv.MicroVMSandbox.available() is True
|
||
assert mv.MicroVMSandbox.unavailable_reason() == ""
|
||
|
||
|
||
def test_macos_does_not_need_kvm(linux: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||
monkeypatch.setattr(mv, "_on_linux", lambda: False)
|
||
assert not linux.exists()
|
||
assert mv.kvm_problem() == ""
|
||
assert mv.MicroVMSandbox.available() is True
|
||
|
||
|
||
def test_runtime_missing_is_reported_before_kvm(linux: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||
monkeypatch.setattr(mv.MicroVMSandbox, "_runtime_installed", staticmethod(lambda: False))
|
||
assert "运行时未就绪" in mv.MicroVMSandbox.unavailable_reason()
|
||
|
||
|
||
def test_auto_skips_microvm_without_kvm(linux: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||
# 运行时装了、镜像也缓存了,只差 KVM:auto 不能选它
|
||
monkeypatch.setattr(mgr.BubblewrapSandbox, "available", staticmethod(lambda: False))
|
||
monkeypatch.setattr(mgr.BubblewrapSandbox, "probe_failure", staticmethod(lambda: ""))
|
||
monkeypatch.setattr(mgr.SeatbeltSandbox, "available", staticmethod(lambda: False))
|
||
monkeypatch.setattr(mgr.settings, "sandbox_backend", "auto")
|
||
manager = mgr.SandboxManager()
|
||
assert isinstance(manager._resolve(), LocalSandbox)
|
||
# 节点上选 strict 档也点不到它
|
||
assert manager._resolve_named("strict") is None
|
||
|
||
|
||
def test_auto_picks_microvm_with_kvm(linux: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||
linux.touch()
|
||
monkeypatch.setattr(mv.os, "access", lambda path, mode: True)
|
||
monkeypatch.setattr(mgr.settings, "sandbox_backend", "auto")
|
||
manager = mgr.SandboxManager()
|
||
assert isinstance(manager._resolve(), mv.MicroVMSandbox)
|
||
|
||
|
||
def test_run_says_why_without_booting(linux: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||
async def must_not_boot(*args, **kwargs): # noqa: ANN002, ANN003, ANN202
|
||
raise AssertionError("没有 KVM 还去启动 VM")
|
||
|
||
monkeypatch.setattr(mv.MicroVMSandbox, "_lease", must_not_boot)
|
||
result = asyncio.run(mv.MicroVMSandbox().run("print(1)"))
|
||
assert result.ok is False
|
||
assert result.error and result.error.startswith("microVM 不可用:") and "/dev/kvm" in result.error
|
||
|
||
|
||
def test_health_reports_missing_kvm(linux: Path) -> None:
|
||
info = asyncio.run(mv.MicroVMSandbox().health())
|
||
assert info["available"] is False
|
||
assert "/dev/kvm" in info["error"]
|
||
assert "KVM" in info["isolation"]
|
||
|
||
|
||
# ---------------- selftest ----------------
|
||
|
||
|
||
def _fake_run(result: ExecResult, calls: list[str]):
|
||
async def run(self, code, **kwargs): # noqa: ANN001, ANN003, ANN202
|
||
calls.append(code)
|
||
return result
|
||
|
||
return run
|
||
|
||
|
||
def test_selftest_without_kvm_does_not_boot(linux: Path, monkeypatch: pytest.MonkeyPatch,
|
||
capsys: pytest.CaptureFixture[str]) -> None:
|
||
calls: list[str] = []
|
||
monkeypatch.setattr(mv.MicroVMSandbox, "run", _fake_run(ExecResult(), calls))
|
||
assert selftest.main() == 1
|
||
assert calls == []
|
||
out = capsys.readouterr().out.strip()
|
||
assert out.startswith("microVM 不可用:") and "/dev/kvm" in out
|
||
|
||
|
||
def test_selftest_passes_when_the_vm_prints_the_mark(linux: Path, monkeypatch: pytest.MonkeyPatch,
|
||
capsys: pytest.CaptureFixture[str]) -> None:
|
||
linux.touch()
|
||
monkeypatch.setattr(mv.os, "access", lambda path, mode: True)
|
||
calls: list[str] = []
|
||
ok = ExecResult(stdout=selftest.MARK + "\n", duration_ms=812, backend="microvm")
|
||
monkeypatch.setattr(mv.MicroVMSandbox, "run", _fake_run(ok, calls))
|
||
assert selftest.main() == 0
|
||
assert len(calls) == 1 and selftest.MARK in calls[0]
|
||
assert "812 ms" in capsys.readouterr().out
|
||
|
||
|
||
def test_selftest_reports_boot_failure(linux: Path, monkeypatch: pytest.MonkeyPatch,
|
||
capsys: pytest.CaptureFixture[str]) -> None:
|
||
linux.touch()
|
||
monkeypatch.setattr(mv.os, "access", lambda path, mode: True)
|
||
failed = ExecResult(ok=False, backend="microvm", error="microVM 启动失败:虚拟机没有应答")
|
||
monkeypatch.setattr(mv.MicroVMSandbox, "run", _fake_run(failed, []))
|
||
assert selftest.main() == 1
|
||
assert capsys.readouterr().out.strip() == "microVM 启动失败:虚拟机没有应答"
|
||
|
||
|
||
def test_selftest_does_not_trust_exit_zero_without_output(linux: Path, monkeypatch: pytest.MonkeyPatch,
|
||
capsys: pytest.CaptureFixture[str]) -> None:
|
||
# 退出码 0 但什么都没打印:VM 里的代码其实没跑起来,不能当成功
|
||
linux.touch()
|
||
monkeypatch.setattr(mv.os, "access", lambda path, mode: True)
|
||
monkeypatch.setattr(mv.MicroVMSandbox, "run", _fake_run(ExecResult(backend="microvm"), []))
|
||
assert selftest.main() == 1
|
||
assert capsys.readouterr().out.strip().startswith("microVM 启动失败:")
|