feat(chv): 阶段 1 数据盘与 tenant 标记同 overlay 解耦(copy 也供)

真机根因(2026-11-07,CHV v54 三变体实证):vm.boot 对任何
带 backing file 的 qcow2 都报 Maximum disk nesting depth
exceeded——overlay-only 与 overlay-rawbase 均 500,base 直接
启动(reuse)22.5 秒成功,数据盘与压缩均已排除。overlay 在
本 VMM 上不可用,持久 VM 须能在 copy 模式下跑。
- _provision_task_disk:copy 与 overlay 都供数据盘并写
  tenant.json(只有 reuse 不供不写);_provision_data_disk_path
  同理(reuse 才返回 None)。
- _cleanup_vm_task_disk:保护(持久登记/登记快照)不再只限
  overlay 模式;无登记无快照的 copy 盘仍按旧语义删除。
- provision_data_disk:数据盘改 raw 稀疏盘(os.truncate,
  零外部依赖)——copy 模式本来就不需要 qemu-img,数据盘
  绝不能悄悄加上这个依赖;CHV 按后缀认 Raw。
- 测试:新增 copy 供数据盘+tenant 与 copy 持久盘 stop 保护;
  VmConfig 三盘断言(任务+seed+数据)更新 arch95/fix 两处
  旧“恰好两盘”契约与本轨 qcow2 类型断言。
回归:test_chv_*.py、test_cloud_hypervisor.py 42、test_chv_v3_fixes
38 全过(全量 cloud/tests 在跑)。
不推、不合、不部署;只本地分支。
This commit is contained in:
host committed 2026-11-08 09:45:00 +00:00
1 parent a5f836e9fd
commit f809014bef
5 files changed
+113 -65

No files matched your search

+22 -21
View File
@@ -17,7 +17,7 @@ check_disk_quota 改为三项:state_dir 已用总量 + 本次
feat-chv-opt-landing(阶段 1):黄金镜像只读 base +
qcow2 overlay 分层(disk_mode="overlay")、数据盘独立
持久(data.qcow2,绝不进快照/GC)、快照与 overlay 的
持久(data.raw,绝不进快照/GC)、快照与 overlay 的
tenant 归属标记(登记条目 tenant 字段 + 每 VM tenant.json
+ 持久 VM 登记 persistent/<tenant>.json)。overlay 与
数据盘只在 overlay 模式下供给;copy/reuse 语义不变。
@@ -149,7 +149,7 @@ def task_disk_path(state_dir: str, vm_name: str) -> str:
def task_disk_paths(state_dir: str, vm_name: str) -> list:
"""该 VM 现存的任务磁盘文件(copy 的 disk.raw 与/或
overlay 的 disk.qcow2):GC 与清理按此清单查,不猜
文件名;数据盘 data.qcow2 不在内——用户数据盘永不
文件名;数据盘 data.raw 不在内——用户数据盘永不
进任务盘 GC,见 delete_data_disk。"""
out = []
for name in ("disk.raw", OVERLAY_DISK_NAME):
@@ -166,7 +166,7 @@ def delete_task_disk(state_dir: str, vm_name: str) -> bool:
绝不静默——残留盘由 gc_stale_disks 下次再清)。
符号链接只删链接本身,不跟随。
feat-chv-opt-landing:disk.raw 与 disk.qcow2 都删
(任一存在即删);数据盘 data.qcow2 绝不在此删——
(任一存在即删);数据盘 data.raw 绝不在此删——
它是用户数据,删它须走 delete_data_disk(显式回收)。"""
removed = False
for path in task_disk_paths(state_dir, vm_name):
@@ -178,14 +178,14 @@ def delete_task_disk(state_dir: str, vm_name: str) -> bool:
# -- overlay 分层与数据盘(feat-chv-opt-landing,阶段 1)------
# 黄金镜像只读 base(qcow2,基线 773 MiB)绝不被写:每台
# 用户 VM 只有一个空 overlay(disk.qcow2,backing file 指
# 向 base);数据盘(data.qcow2,工作区 + 浏览器 profile
# 向 base);数据盘(data.raw,工作区 + 浏览器 profile
# 的载体)是第二块独立盘——快照登记与 GC 只认任务盘,
# 数据盘不进快照、不进 GC、不随 stop 删(删它须显式回收)。
# 客内挂载见镜像 99-chv-sync.cfg runcmd 的 chv-data-mount
#(按 LABEL=CHVDATA 找盘,不依赖 /dev/vd? 固定名)。
OVERLAY_DISK_NAME = "disk.qcow2"
DATA_DISK_NAME = "data.qcow2"
DATA_DISK_NAME = "data.raw"
DATA_DISK_GB = 10.0
DATA_DISK_LABEL = "CHVDATA"
TENANT_FILE_NAME = "tenant.json"
@@ -198,7 +198,7 @@ def overlay_disk_path(state_dir: str, vm_name: str) -> str:
def data_disk_path(state_dir: str, vm_name: str) -> str:
"""该 VM 的数据盘路径:state_dir/<vm>/data.qcow2。"""
"""该 VM 的数据盘路径:state_dir/<vm>/data.raw。"""
return os.path.join(state_dir, vm_name, DATA_DISK_NAME)
@@ -308,30 +308,31 @@ def create_overlay_disk(base_path: str, overlay_path: str,
def provision_data_disk(state_dir: str, vm_name: str,
size_gb: float = DATA_DISK_GB,
runner: Callable[..., object] | None = None) -> str:
"""建数据盘(空 qcow2 稀疏盘,虚大小默认 10 GB,实际
占用随写入增长):已存在即复用(不 resize、不重建,
旧盘里可能已有用户数据);不存在即 qemu-img create。
size_gb: float = DATA_DISK_GB) -> str:
"""建数据盘(空 raw 稀疏盘,虚大小默认 10 GB,实际占用
随写入增长):已存在即复用(不 resize、不重建,旧盘里
可能已有用户数据);不存在即 os.truncate 建稀疏文件。
raw 而不用 qcow2(2026-11-07):数据盘只是空白可写卷,
qcow2 在此无收益;raw 零外部依赖——copy 模式本来就不
需要 qemu-img,数据盘绝不能悄悄给 copy 加上这个依赖
(Muse VM 无 qemu-img 时 copy 派发会全灭;CHV v54 又
拒绝带 backing 的 qcow2,数据盘走 qcow2 毫无必要)。
失败即 ChvFailure disk_overlay_invalid(数据盘建不出
来时 overlay 模式拒绝启动,不让任务在无数据盘的
VM 里把工作区写进系统盘——静默回落等于丢“独立持久”
的保证)。"""
run = runner if runner is not None else _default_qemu_runner
qemu = "qemu-img" if runner is not None else _qemu_img_cmd()
来时拒绝启动,不让任务在无数据盘的 VM 里把工作区写进
系统盘——静默回落等于丢“独立持久”的保证)。"""
path = data_disk_path(state_dir, vm_name)
os.makedirs(os.path.dirname(path), exist_ok=True)
if os.path.islink(path):
os.remove(path)
if os.path.exists(path):
return path
result = run([qemu, "create", "-f", "qcow2",
path, "%gG" % size_gb])
if getattr(result, "returncode", 1) != 0:
try:
with open(path, "wb") as fh:
fh.truncate(int(size_gb * 1024 ** 3))
except OSError as exc:
raise ChvFailure(
"disk_overlay_invalid",
"数据盘建盘失败:%s:%s"
% (path, getattr(result, "stderr", "") or ""))
"数据盘建盘失败:%s:%s" % (path, exc))
if not os.path.exists(path):
raise ChvFailure(
"disk_overlay_invalid",