fix(chv): 生产级审核 P1-01~P1-10 修复(堆叠于 #270 之上)

This commit is contained in:
muse-agent committed 2026-11-02 03:25:49 +00:00
1 parent 8c8d62f70d
commit 971e054938
12 files changed
+1545 -120

No files matched your search

+78 -24
View File
@@ -165,10 +165,21 @@ def _save_snapshots(state_dir, vm_name, entries):
def prune_snapshots(state_dir: str, vm_name: str,
retention_days: float | None,
now: float | None = None,
delete_files: bool = True) -> tuple[list, list]:
delete_files: bool = True,
events: list | None = None) -> tuple[list, list]:
"""按保留期剪枝:删过期条目;delete_files 时同时删
对应的 file:// 快照文件(只删登记里记的 path,不猜
文件名)。返回(保留条目,剪掉条目)。"""
文件名)。返回(保留条目,剪掉条目)。
feat-chv-p1fix(P1-08):删文件动作不再静默——
events 非空列表时,每条剪枝追加一条事件:
("deleted", entry, None) / ("delete_failed", entry,
error) / ("pruned_no_file", entry, None)(登记条目
被剪、但无对应文件可删)。调用方(executor.snapshot_vm)
据此发 vm_snapshot_pruned / vm_snapshot_prune_failed
审计;events 为 None 时行为与旧版一致(仍删、仍
best-effort),不改旧调用点返回值。
"""
import time
now = time.time() if now is None else now
kept, pruned = [], []
@@ -181,8 +192,15 @@ def prune_snapshots(state_dir: str, vm_name: str,
try:
if os.path.exists(entry["path"]):
os.remove(entry["path"])
except OSError:
pass
if events is not None:
events.append(("deleted", entry, None))
elif events is not None:
events.append(("pruned_no_file", entry, None))
except OSError as exc:
if events is not None:
events.append(("delete_failed", entry, exc))
elif events is not None:
events.append(("pruned_no_file", entry, None))
else:
kept.append(entry)
if pruned:
@@ -192,13 +210,16 @@ def prune_snapshots(state_dir: str, vm_name: str,
def check_snapshot_limits(state_dir: str, vm_name: str, max_count: int,
max_total_gb: float,
retention_days: float | None) -> list:
retention_days: float | None,
events: list | None = None) -> list:
"""快照上限预检(snapshot_vm 调 ch-remote 之前调):
先按保留期剪枝,再查数量(已有数 + 本次 > 上限即
snapshot_quota_exceeded)与总量(已有总量已达上限即
拒绝;本次大小事前未知,总量在 record 时二次收紧,
见 record_snapshot)。返回剪枝后条目。"""
kept, _ = prune_snapshots(state_dir, vm_name, retention_days)
见 record_snapshot)。返回剪枝后条目。events 透传给
prune_snapshots(见 P1-08),供调用方审计剪枝动作。"""
kept, _ = prune_snapshots(state_dir, vm_name, retention_days,
events=events)
if len(kept) + 1 > max_count:
raise ChvFailure(
"snapshot_quota_exceeded",
@@ -221,11 +242,24 @@ def check_snapshot_limits(state_dir: str, vm_name: str, max_count: int,
return kept
def record_snapshot(state_dir: str, vm_name: str, snapshot_url: str, max_total_gb: float) -> tuple[list, list]:
def record_snapshot(state_dir: str, vm_name: str, snapshot_url: str, max_total_gb: float,
events: list | None = None) -> tuple[list, list]:
"""快照成功后登记;file:// 目标记实际大小。登记后若
总量超上限,按最旧优先删登记外的旧快照文件(总量硬
帽,删动作由调用方审计——本函数返回被删的旧条目)。
返回(全部条目,被总量帽删掉的旧条目)。"""
返回(全部条目,被总量帽删掉的旧条目)。
feat-chv-p1fix(P1-06):单条快照本身大于总量帽时,
在任何落盘/删旧动作之前直接拒绝
(snapshot_quota_exceeded),不悄悄保留超大快照——
旧循环 `while len > 1` 在单条超帽时直接停、超大快照
被保留。大小未知(size 为 None,如远端目标或文件
缺失)时无法判定,不拒绝,沿用旧口径。
feat-chv-p1fix(P1-08):总量帽删文件动作记 events
(("deleted"/"delete_failed"/"pruned_no_file", entry,
error)),供调用方审计;删失败时登记条目仍移除
(旧语义不变),但失败本身不再静默。
"""
import time
path = None
size = None
@@ -235,23 +269,43 @@ def record_snapshot(state_dir: str, vm_name: str, snapshot_url: str, max_total_g
size = os.path.getsize(path)
except OSError:
size = None
if max_total_gb is not None and size is not None \
and size > max_total_gb * 1024 ** 3:
raise ChvFailure(
"snapshot_quota_exceeded",
"单条快照 %d 字节已超过总量上限 %.1f GB(VM %s),"
"拒绝记录" % (size, max_total_gb, vm_name))
entries = _load_snapshots(state_dir, vm_name)
entries.append({"url": snapshot_url, "path": path,
"size": size, "created_at": time.time()})
_save_snapshots(state_dir, vm_name, entries)
removed = []
if max_total_gb is not None:
cap = max_total_gb * 1024 ** 3
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
total -= oldest.get("size") or 0
removed.append(oldest)
if removed:
_save_snapshots(state_dir, vm_name, entries)
removed = _tighten_snapshot_cap(entries, max_total_gb, events)
if removed:
_save_snapshots(state_dir, vm_name, entries)
return entries, removed
def _tighten_snapshot_cap(entries, max_total_gb, events):
"""总量帽收紧(record_snapshot 提取,守 50 行门禁):
总量超帽时按最旧优先删登记外的旧快照文件,返回被删
的旧条目;删动作记 events(见 record_snapshot)。"""
removed = []
if max_total_gb is None:
return removed
cap = max_total_gb * 1024 ** 3
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"])
if events is not None:
events.append(("deleted", oldest, None))
except OSError as exc:
if events is not None:
events.append(("delete_failed", oldest, exc))
elif events is not None:
events.append(("pruned_no_file", oldest, None))
total -= oldest.get("size") or 0
removed.append(oldest)
return removed