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:
1 parent
ef206cf554
commit
65504ae8cb
10 files changed
+962
-91
No files matched your search
+48
-19
@@ -141,6 +141,34 @@ def _snapshots_path(state_dir, vm_name):
|
||||
return os.path.join(state_dir, vm_name, "snapshots.json")
|
||||
|
||||
|
||||
def _snapshot_path_size(path):
|
||||
"""快照产物大小:v54 起快照是目录(含 config.json/
|
||||
state.json/memory-ranges),取目录内文件总和;文件
|
||||
目标(旧登记)仍取文件大小;读不出时返回 None(调用
|
||||
方按未知处理,不谎报 0)。"""
|
||||
if os.path.isdir(path) and not os.path.islink(path):
|
||||
return _dir_total_bytes(path)
|
||||
try:
|
||||
return os.path.getsize(path)
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def _remove_snapshot_path(path):
|
||||
"""删快照产物:目录整树删,文件/链接只删本身。"""
|
||||
if os.path.isdir(path) and not os.path.islink(path):
|
||||
shutil.rmtree(path)
|
||||
elif os.path.islink(path) or os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def has_recorded_snapshots(state_dir: str, vm_name: str) -> bool:
|
||||
"""该 VM 是否还有登记在册的快照(hibernate 的盘保护
|
||||
依据):有登记快照时任务盘是 restore 的输入,残留盘
|
||||
GC 不得删;登记被剪干净后盘即成孤儿,可正常 GC。"""
|
||||
return bool(_load_snapshots(state_dir, vm_name))
|
||||
|
||||
|
||||
def _load_snapshots(state_dir, vm_name):
|
||||
import json
|
||||
path = _snapshots_path(state_dir, vm_name)
|
||||
@@ -176,12 +204,8 @@ def prune_snapshots(state_dir: str, vm_name: str,
|
||||
if retention_days is not None \
|
||||
and now - created > retention_days * 86400:
|
||||
pruned.append(entry)
|
||||
if delete_files and entry.get("path"):
|
||||
try:
|
||||
if os.path.exists(entry["path"]):
|
||||
os.remove(entry["path"])
|
||||
except OSError:
|
||||
pass
|
||||
if delete_files:
|
||||
_prune_snapshot_file(entry)
|
||||
else:
|
||||
kept.append(entry)
|
||||
if pruned:
|
||||
@@ -189,6 +213,19 @@ def prune_snapshots(state_dir: str, vm_name: str,
|
||||
return kept, pruned
|
||||
|
||||
|
||||
def _prune_snapshot_file(entry):
|
||||
"""剪枝时删一条登记对应的快照产物:目录整树删,
|
||||
文件只删本身;删失败静默(登记已剪,残留产物由
|
||||
下次 GC/人工清理,不因删产物失败挡住剪枝本身)。"""
|
||||
path = entry.get("path")
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
_remove_snapshot_path(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def check_snapshot_limits(state_dir: str, vm_name: str, max_count: int,
|
||||
max_total_gb: float,
|
||||
retention_days: float | None) -> list:
|
||||
@@ -208,10 +245,9 @@ def check_snapshot_limits(state_dir: str, vm_name: str, max_count: int,
|
||||
if isinstance(size, (int, float)):
|
||||
total += size
|
||||
elif entry.get("path") and os.path.exists(entry["path"]):
|
||||
try:
|
||||
total += os.path.getsize(entry["path"])
|
||||
except OSError:
|
||||
pass
|
||||
_sized = _snapshot_path_size(entry["path"])
|
||||
if _sized is not None:
|
||||
total += _sized
|
||||
if total >= max_total_gb * 1024 ** 3:
|
||||
raise ChvFailure(
|
||||
"snapshot_quota_exceeded",
|
||||
@@ -230,10 +266,7 @@ def record_snapshot(state_dir: str, vm_name: str, snapshot_url: str, max_total_g
|
||||
size = None
|
||||
if snapshot_url.startswith("file://"):
|
||||
path = snapshot_url[len("file://"):]
|
||||
try:
|
||||
size = os.path.getsize(path)
|
||||
except OSError:
|
||||
size = None
|
||||
size = _snapshot_path_size(path)
|
||||
entries = _load_snapshots(state_dir, vm_name)
|
||||
entries.append({"url": snapshot_url, "path": path,
|
||||
"size": size, "created_at": time.time()})
|
||||
@@ -244,11 +277,7 @@ def record_snapshot(state_dir: str, vm_name: str, snapshot_url: str, max_total_g
|
||||
total = sum(e.get("size") or 0 for e in entries)
|
||||
while len(entries) > 1 and total > cap:
|
||||
oldest = entries.pop(0)
|
||||
if oldest.get("path") and os.path.exists(oldest["path"]):
|
||||
try:
|
||||
os.remove(oldest["path"])
|
||||
except OSError:
|
||||
pass
|
||||
_prune_snapshot_file(oldest)
|
||||
total -= oldest.get("size") or 0
|
||||
removed.append(oldest)
|
||||
if removed:
|
||||
|
||||
Reference in new issue
Block a user