fix(chv): V-3/F1/F2 真机缺陷修复 + V-1/V-4 附带修复

V-3 快照/restore:
- snapshot 改传裸 URL(source_url= 是 restore 的键,服务端
  Could not extract path from URL);快照产物是目录,校验
  改为目录非空,登记计量/剪枝同步支持目录
- restore 起 VMM 前补 _restore_prepare:杀旧 VMM(登记的 +
  未登记跨实例扫描)并清旧 socket,与 _wake_prepare 对等
- hibernate 评估结论:快照 config.json 引用原盘,旧 stop 删
  盘致 restore 必失败;hibernate 的 stop 改 keep_disk=True,
  GC 不删有登记快照的 VM 的盘,restore 前预检快照引用的盘
  (snapshot_disk_missing fail-fast)

F1:re-wake 删旧盘——H5 删私钥致 re-wake 必然生成新 key 对,
旧盘复用时 cloud-init 不重应用新 seed,sshd 仍呈旧 key 而
known_hosts 钉新 key;wake 本就是冷启动,旧盘不是要保留的状态

F2:stop_vm 跨实例按 socket 进程扫描杀未登记 VMM
(/proc cmdline 精确 socket + VMM 二进制名 + 跳过自身/pid 1,
绝不误杀 ch-remote 与他人 VMM)

V-4:remote_command source 失败改 || exit 126 fail-fast;
guest-env 条目 owner 改归 SSH 用户(0600 不变),root 仍 root:root

V-1 附带:build.sh 预建 /app/work(1777 粘滞位 + stat 断言);
host.py default_codex_bin() 改 shutil.which,裸名不再让
health 报 ok:false

测试:新 test_chv_v3_fixes.py 23 项;旧快照测试按目录产物改写;
test_chv* 458 全过,全量 cloud 套件 1930 全过(skipped=8)
This commit is contained in:
host committed 2026-11-08 09:45:00 +00:00
1 parent ef206cf554
commit 65504ae8cb
10 files changed
+962 -91

No files matched your search

+21 -10
View File
@@ -392,16 +392,24 @@ class RestoreSnapshotTest(CloseupCase):
self.assertEqual(self.launches, [])
def test_snapshot_not_persisted_missing_and_empty(self):
# feat-chv-v3:v54 快照产物是目录,校验改为“目录存在
# 且非空”——文件目标/空目录/缺失一律 snapshot_not_persisted。
ex = self.make_exec()
missing = "file://" + os.path.join(self.state_dir, "nope.raw")
missing = "file://" + os.path.join(self.state_dir, "nope.snap")
os.makedirs(self.state_dir, exist_ok=True)
with self.assertRaises(ChvFailure) as ctx:
ex.snapshot_vm(self._task(), missing)
self.assertEqual(ctx.exception.kind, "snapshot_not_persisted")
empty_path = os.path.join(self.state_dir, "empty.raw")
open(empty_path, "wb").close()
empty_dir = os.path.join(self.state_dir, "empty.snap")
os.makedirs(empty_dir, exist_ok=True)
with self.assertRaises(ChvFailure) as ctx:
ex.snapshot_vm(self._task(), "file://" + empty_path)
ex.snapshot_vm(self._task(), "file://" + empty_dir)
self.assertEqual(ctx.exception.kind, "snapshot_not_persisted")
file_target = os.path.join(self.state_dir, "file.snap")
with open(file_target, "wb") as fh:
fh.write(b"S" * 60)
with self.assertRaises(ChvFailure) as ctx:
ex.snapshot_vm(self._task(), "file://" + file_target)
self.assertEqual(ctx.exception.kind, "snapshot_not_persisted")
def test_snapshot_prunes_oldest_on_total_cap(self):
@@ -416,15 +424,18 @@ class RestoreSnapshotTest(CloseupCase):
json.dump([{"url": "file://" + old_path, "path": old_path,
"size": 100, "created_at": time.time()}], fh)
# 上限 150 字节:预检时旧 100 字节通过,新 60 字节记入后
# 总量 160 超帽,最旧一条被剪枝并审计。
# 总量 160 超帽,最旧一条被剪枝并审计。feat-chv-v3:
# 新快照是目录(config.json + state.json 各 30 字节)。
cap_gb = 150 / (1024 ** 3)
ex = self.make_exec(env_extra={
"CHV_SNAPSHOT_MAX_TOTAL_GB": repr(cap_gb)})
new_path = os.path.join(self.state_dir, "new.raw")
with open(new_path, "wb") as fh:
fh.write(b"N" * 60)
out = ex.snapshot_vm(self._task(), "file://" + new_path)
self.assertEqual(out, "file://" + new_path)
new_dir = os.path.join(self.state_dir, "new.snap")
os.makedirs(new_dir, exist_ok=True)
for name in ("config.json", "state.json"):
with open(os.path.join(new_dir, name), "wb") as fh:
fh.write(b"N" * 30)
out = ex.snapshot_vm(self._task(), "file://" + new_dir)
self.assertEqual(out, "file://" + new_dir)
self.assertFalse(os.path.exists(old_path))
self.assertIn("vm_snapshot_pruned", self._audit_events())