堆叠于 #246(feat-chv-secfix),只到 PR OPEN,未合并、未部署。 安全语义不变:egress、key seed 投递、磁盘删除、默认 provisioned、 tap 校验一律保留;前端零改动。 - WarmPool 定案为实验性(ADR-5):dispatch 永远冷启动, wired_to_dispatch 恒 False,acquire/resume 只保留实验接口, worker 仍调 reconcile_orphans 清理残留。 - 长函数提取:preflight 126→31、wake_vm 135→36、__init__ 69→31、 build_chv_config 159→58;错误文案逐字不变。 - 严格测试替身:fake API 校验 VmConfig(任务盘+seed 盘、net 无重复),fake SSH 校验 known_hosts 钉死;新增 5 项测试。 - Docker 同步守卫:8 个真实标记用例下 Docker 内联版与正典 parse_goal_result 状态全等,改分叉即 fail。 - 配置统一校验:preflight 以 config_invalid 统一兜底端口/配额/ 超时/快照/host-key 模式(构造时已校验正常值,preflight 兜底 事后改写);CHV_NET 与 CHV_NET_JSON 双设仍警告不硬拒。 - 类型与 lint:chv 叶子模块公开 API 补类型注解;stdlib 扫描 揪出 executor 内 1 个真死 import(host_key_paths)并删除。 - ADR:docs/chv-architecture.md(ADR-1 至 ADR-7)。 测试:CHV 163 项全过;cloud 全量 1593 项三跑结果为 8 errors (首跑详细日志因 tail 截断丢失)、全过、1 失败(浏览器代理 时序测试,与 CHV 无关),全量本身存在时序偶发,CHV 套件 确定性全过。
259 lines
10 KiB
Python
259 lines
10 KiB
Python
"""CHV 架构 9.5 提升轨测试(feat-chv-arch95,堆叠于 #246)。
|
||
|
||
只用标准库与 mock,绝不启动真实 VM。本文件是“严格测试替身”
|
||
的载体:替身不再无条件放行——fake API 校验 VmConfig 内容
|
||
(任务盘 + seed 盘、net 条目),fake SSH 校验 known_hosts
|
||
钉死与 StrictHostKeyChecking=yes,mock 再也不能把被测逻辑
|
||
本身也 mock 掉。测试里只用 FAKE 占位密钥,绝无真实密钥。
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
from pathlib import Path
|
||
|
||
try:
|
||
from cloud.chv.pool import WarmPool
|
||
from cloud.sandbox import (
|
||
ChvFailure,
|
||
CloudHypervisorExecutor,
|
||
DockerExecutor,
|
||
parse_goal_result,
|
||
)
|
||
except ImportError:
|
||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||
from cloud.chv.pool import WarmPool
|
||
from cloud.sandbox import (
|
||
ChvFailure,
|
||
CloudHypervisorExecutor,
|
||
DockerExecutor,
|
||
parse_goal_result,
|
||
)
|
||
|
||
|
||
class FakeResult:
|
||
def __init__(self, returncode=0, stdout="", stderr=""):
|
||
self.returncode = returncode
|
||
self.stdout = stdout
|
||
self.stderr = stderr
|
||
|
||
|
||
class FakeProc:
|
||
def __init__(self, alive=True):
|
||
self._alive = alive
|
||
|
||
def poll(self):
|
||
return None if self._alive else 1
|
||
|
||
def terminate(self):
|
||
self._alive = False
|
||
|
||
def wait(self, timeout=None):
|
||
return 0
|
||
|
||
def kill(self):
|
||
self._alive = False
|
||
|
||
|
||
def _write(path, content=b"x"):
|
||
with open(path, "wb") as fh:
|
||
fh.write(content)
|
||
os.chmod(path, 0o755)
|
||
return path
|
||
|
||
|
||
class Arch95Case(unittest.TestCase):
|
||
def setUp(self):
|
||
self.tmp = tempfile.TemporaryDirectory()
|
||
self.dir = self.tmp.name
|
||
self.binary = _write(os.path.join(self.dir, "cloud-hypervisor"))
|
||
self.ch_remote = _write(os.path.join(self.dir, "ch-remote"))
|
||
self.kernel = _write(os.path.join(self.dir, "vmlinux"))
|
||
self.rootfs = _write(os.path.join(self.dir, "disk.raw"), b"R" * 100)
|
||
self.ssh_key = _write(os.path.join(self.dir, "id_chv"))
|
||
self.kvm = _write(os.path.join(self.dir, "kvm"))
|
||
self.cpuinfo = os.path.join(self.dir, "cpuinfo")
|
||
with open(self.cpuinfo, "w") as fh:
|
||
fh.write("flags : fpu vmx sse\n")
|
||
self.state_dir = os.path.join(self.dir, "state")
|
||
self.calls = []
|
||
self.audit = []
|
||
self.api_payloads = []
|
||
|
||
def tearDown(self):
|
||
self.tmp.cleanup()
|
||
|
||
def _runner(self, cmd, timeout=None):
|
||
self.calls.append(list(cmd))
|
||
if cmd and cmd[0] == "ssh-keygen":
|
||
priv = cmd[cmd.index("-f") + 1]
|
||
with open(priv, "w") as fh:
|
||
fh.write("-----BEGIN OPENSSH PRIVATE KEY-----\nFAKE\n")
|
||
os.chmod(priv, 0o600)
|
||
with open(priv + ".pub", "w") as fh:
|
||
fh.write("ssh-ed25519 AAAAfake1 vm-key\n")
|
||
return FakeResult(0, "", "")
|
||
if cmd and cmd[0] == "genisoimage":
|
||
out = cmd[cmd.index("-output") + 1]
|
||
with open(out, "wb") as fh:
|
||
fh.write(b"CIDATA" * 16)
|
||
return FakeResult(0, "", "")
|
||
return FakeResult(0, "", "")
|
||
|
||
def _strict_api(self, socket_path, method, path, payload=None):
|
||
"""严格替身:vm.create 必须带任务盘 + seed 盘、net 无重复。"""
|
||
if path == "/vm.create":
|
||
self.api_payloads.append(payload)
|
||
disks = [d.get("path") for d in payload.get("disks", [])]
|
||
self.assertEqual(len(disks), 2, "VmConfig 必须恰好两盘")
|
||
self.assertTrue(disks[0].endswith("disk.raw"),
|
||
"第一盘必须是任务盘")
|
||
self.assertTrue(disks[1].endswith("seed.iso"),
|
||
"第二盘必须是 seed 盘,缺 seed 即假保证")
|
||
self.assertTrue(payload.get("kernel", {}).get("path"))
|
||
nets = payload.get("net", [])
|
||
taps = [n.get("tap") for n in nets]
|
||
self.assertEqual(len(taps), len(set(taps)), "net tap 重复")
|
||
return (200, {})
|
||
|
||
def make_exec(self, api=None, runner=None, env_extra=None):
|
||
env = {"CHV_EGRESS_PROXY": "http://10.0.0.1:8888",
|
||
"CHV_SSH_FORWARD": "1"}
|
||
if env_extra:
|
||
env.update(env_extra)
|
||
ex = CloudHypervisorExecutor(
|
||
binary=self.binary, ch_remote=self.ch_remote,
|
||
kernel=self.kernel, rootfs=self.rootfs,
|
||
ssh_key=self.ssh_key, kvm_path=self.kvm,
|
||
cpuinfo_path=self.cpuinfo, state_dir=self.state_dir,
|
||
runner=runner or self._runner,
|
||
launcher=lambda cmd: FakeProc(alive=True),
|
||
api_client=api or self._strict_api,
|
||
sleeper=lambda s: None, env=env)
|
||
ex.audit_callback = lambda e, t, d, k=None: self.audit.append(
|
||
(e, t.get("id") if isinstance(t, dict) else t, d, k))
|
||
return ex
|
||
|
||
|
||
class WarmPoolDecisionTest(Arch95Case):
|
||
def test_pool_is_experimental_and_dispatch_is_cold_only(self):
|
||
self.assertFalse(WarmPool.wired_to_dispatch)
|
||
ex = self.make_exec()
|
||
pool = WarmPool(ex)
|
||
# 手工放一个 warm 条目:dispatch 必须不消耗它(冷启动路径)。
|
||
pool._entries["warm-1"] = {"state": "warm",
|
||
"task": {"id": "warm-1"},
|
||
"vm": "chv-warm-1",
|
||
"snapshot_url": None}
|
||
ex.wake_vm({"id": "t1"})
|
||
self.assertEqual(pool._entries["warm-1"]["state"], "warm")
|
||
self.assertEqual(pool.warm_count(), 1)
|
||
|
||
|
||
class StrictVmConfigTest(Arch95Case):
|
||
def test_wake_vm_config_has_task_disk_and_seed_disk(self):
|
||
ex = self.make_exec()
|
||
out = ex.wake_vm({"id": "t1"})
|
||
self.assertEqual(out["state"], "running")
|
||
self.assertEqual(len(self.api_payloads), 1)
|
||
# known_hosts 钉死:文件存在且含预置公钥,SSH 命令带钉死选项。
|
||
kh = ex.known_hosts_path({"id": "t1"})
|
||
with open(kh) as fh:
|
||
content = fh.read()
|
||
self.assertIn("AAAAfake1", content)
|
||
cmd = ex.build_ssh_command({"id": "t1"}, ["true"])
|
||
self.assertIn("StrictHostKeyChecking=yes", " ".join(cmd))
|
||
self.assertIn(kh, " ".join(cmd))
|
||
|
||
def test_strict_ssh_runner_rejects_missing_known_hosts(self):
|
||
def strict_runner(cmd, timeout=None):
|
||
if cmd and cmd[0] == "ssh":
|
||
self.assertIn("StrictHostKeyChecking=yes", cmd)
|
||
found = False
|
||
for i, a in enumerate(cmd):
|
||
if a.startswith("UserKnownHostsFile="):
|
||
path = a.split("=", 1)[1]
|
||
found = True
|
||
elif a == "UserKnownHostsFile":
|
||
path = cmd[i + 1]
|
||
found = True
|
||
else:
|
||
continue
|
||
with open(path) as fh:
|
||
self.assertTrue(fh.read().strip(),
|
||
"known_hosts 为空即假钉死")
|
||
self.assertTrue(found, "SSH 命令缺 known_hosts 钉死选项")
|
||
return self._runner(cmd, timeout=timeout)
|
||
ex = self.make_exec(runner=strict_runner)
|
||
ex.wake_vm({"id": "t1"})
|
||
# 严格 runner 在 _run_guest 里再跑一次 status 风格调用:
|
||
# 这里直接验证 build 出的命令能过严格 runner 的检查。
|
||
cmd = ex.build_ssh_command({"id": "t1"}, ["true"])
|
||
strict_runner(cmd)
|
||
|
||
|
||
class ConfigUnifiedTest(Arch95Case):
|
||
def test_preflight_unified_config_invalid(self):
|
||
cases = [
|
||
("ssh_port", 0), ("ssh_port", 70000),
|
||
("max_vms", -1), ("disk_gb", 0),
|
||
("api_timeout", 0), ("ready_timeout", -1),
|
||
("ready_interval", 0), ("warm_pool_size", -1),
|
||
("snapshot_max_count", -1),
|
||
("snapshot_max_total_gb", -1),
|
||
("snapshot_retention_days", -1),
|
||
("host_key_mode", "bogus"),
|
||
]
|
||
for field, bad in cases:
|
||
with self.subTest(field=field, bad=bad):
|
||
ex = self.make_exec()
|
||
setattr(ex, field, bad)
|
||
with self.assertRaises(ChvFailure) as ctx:
|
||
ex.preflight()
|
||
self.assertEqual(ctx.exception.kind, "config_invalid")
|
||
|
||
|
||
class DockerSyncGuardTest(unittest.TestCase):
|
||
"""Docker goal 内联版与正典 parse_goal_result 的同步守卫:
|
||
7 个真实标记用例 + 已存在撞名用例,两者状态必须全等;
|
||
任一实现改了分叉,测试即 fail(不再只靠文档清单)。"""
|
||
|
||
def _docker_status(self, stdout, returncode, stderr=""):
|
||
old = os.environ.get("XCM_API_KEY")
|
||
os.environ["XCM_API_KEY"] = "FAKE-TEST-KEY"
|
||
try:
|
||
ex = DockerExecutor(runner=lambda cmd, timeout=None:
|
||
FakeResult(returncode, stdout, stderr))
|
||
ex._call_runner = lambda cmd, timeout=None, env=None: \
|
||
FakeResult(returncode, stdout, stderr)
|
||
return ex.goal_task({"id": "t1", "goal": "do it",
|
||
"goal_max_rounds": 3})["status"]
|
||
finally:
|
||
if old is None:
|
||
del os.environ["XCM_API_KEY"]
|
||
else:
|
||
os.environ["XCM_API_KEY"] = old
|
||
|
||
def test_docker_and_canonical_goal_status_equal(self):
|
||
cases = [
|
||
("goal met after rounds\n", 0, ""),
|
||
("goal unmet after rounds\nround 1\nverify_out=fail\n", 0, ""),
|
||
("goal interrupted after rounds: boom\n", 1, ""),
|
||
("", 1, "boom"),
|
||
("KeyError: breakpoint\n", 1, ""),
|
||
("", 0, ""),
|
||
("goal-resume 拒绝:breakpoint lost\n", 1, ""),
|
||
("任务已存在\n", 1, ""),
|
||
]
|
||
for stdout, rc, stderr in cases:
|
||
with self.subTest(stdout=stdout[:30], rc=rc):
|
||
canon = parse_goal_result(
|
||
"t1", FakeResult(rc, stdout, stderr), "local")
|
||
docker = self._docker_status(stdout, rc, stderr)
|
||
self.assertEqual(docker, canon["status"])
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|