fix(chv): 生产级审核 P1-01~P1-10 修复(堆叠于 #270 之上)
This commit is contained in:
1 parent
8c8d62f70d
commit
971e054938
12 files changed
+1545
-120
No files matched your search
+78
-24
@@ -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
|
||||
Reference in new issue
Block a user