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:
1 parent
a5f836e9fd
commit
f809014bef
5 files changed
+113
-65
No files matched your search
@@ -102,15 +102,20 @@ class Arch95Case(unittest.TestCase):
|
||||
return FakeResult(0, "", "")
|
||||
|
||||
def _strict_api(self, socket_path, method, path, payload=None):
|
||||
"""严格替身:vm.create 必须带任务盘 + seed 盘、net 无重复。"""
|
||||
"""严格替身:vm.create 必须带任务盘 + seed 盘 + 数据盘、
|
||||
net 无重复(阶段 1:数据盘是第三块盘,见
|
||||
_provision_task_disk 的解耦说明)。"""
|
||||
if path == "/vm.create":
|
||||
self.api_payloads.append(payload)
|
||||
disks = [d.get("path") for d in payload.get("disks", [])]
|
||||
self.assertEqual(len(disks), 2, "VmConfig 必须恰好两盘")
|
||||
self.assertEqual(len(disks), 3,
|
||||
"VmConfig 必须恰好三盘(任务+seed+数据)")
|
||||
self.assertTrue(disks[0].endswith("disk.raw"),
|
||||
"第一盘必须是任务盘")
|
||||
self.assertTrue(disks[1].endswith("seed.iso"),
|
||||
"第二盘必须是 seed 盘,缺 seed 即假保证")
|
||||
self.assertTrue(disks[2].endswith("data.raw"),
|
||||
"第三盘必须是数据盘")
|
||||
self.assertTrue(payload.get("payload", {}).get("kernel"))
|
||||
nets = payload.get("net", [])
|
||||
taps = [n.get("tap") for n in nets]
|
||||
|
||||
@@ -176,11 +176,15 @@ class SeedDiskTest(ChvFixCase):
|
||||
out = ex.wake_vm({"id": "seed1"})
|
||||
self.assertEqual(out["state"], "running")
|
||||
payload = self.api_payloads["vm.create"]
|
||||
self.assertEqual(len(payload["disks"]), 2)
|
||||
self.assertEqual(len(payload["disks"]), 3)
|
||||
seed_disk = payload["disks"][1]["path"]
|
||||
self.assertTrue(seed_disk.endswith("seed.iso"))
|
||||
self.assertTrue(payload["disks"][1]["readonly"])
|
||||
self.assertTrue(os.path.exists(seed_disk))
|
||||
# 第三盘是数据盘(阶段 1:copy 模式也挂数据盘)。
|
||||
self.assertTrue(payload["disks"][2]["path"].endswith(
|
||||
"data.raw"))
|
||||
self.assertEqual(payload["disks"][2]["image_type"], "Raw")
|
||||
self.assertIn("genisoimage", [c[0] for c in self.calls])
|
||||
|
||||
def test_seed_disk_build_failure_is_fail_fast(self):
|
||||
|
||||
@@ -268,19 +268,19 @@ class OverlayDiskTest(unittest.TestCase):
|
||||
self.assertEqual(disk,
|
||||
os.path.join(self.state, "chv-a", "disk.qcow2"))
|
||||
self.assertTrue(os.path.exists(disk))
|
||||
data = provision_data_disk(self.state, "chv-a", runner=runner)
|
||||
data = provision_data_disk(self.state, "chv-a")
|
||||
self.assertEqual(data,
|
||||
os.path.join(self.state, "chv-a", "data.qcow2"))
|
||||
os.path.join(self.state, "chv-a", "data.raw"))
|
||||
self.assertTrue(os.path.exists(data))
|
||||
# 数据盘已存在即复用,不重建。
|
||||
data2 = provision_data_disk(self.state, "chv-a", runner=runner)
|
||||
data2 = provision_data_disk(self.state, "chv-a")
|
||||
self.assertEqual(data2, data)
|
||||
|
||||
def test_delete_task_disk_removes_both_but_not_data(self):
|
||||
runner = _fake_qemu_runner_factory({})
|
||||
provision_task_disk(self.state, "chv-a", self.base,
|
||||
"overlay", runner=runner)
|
||||
provision_data_disk(self.state, "chv-a", runner=runner)
|
||||
provision_data_disk(self.state, "chv-a")
|
||||
raw = os.path.join(self.state, "chv-a", "disk.raw")
|
||||
with open(raw, "wb") as fh:
|
||||
fh.write(b"R")
|
||||
@@ -290,7 +290,7 @@ class OverlayDiskTest(unittest.TestCase):
|
||||
self.assertFalse(os.path.exists(raw))
|
||||
# 数据盘还在:删任务盘绝不删用户数据。
|
||||
self.assertTrue(os.path.exists(
|
||||
os.path.join(self.state, "chv-a", "data.qcow2")))
|
||||
os.path.join(self.state, "chv-a", "data.raw")))
|
||||
self.assertTrue(delete_data_disk(self.state, "chv-a"))
|
||||
self.assertFalse(delete_task_disk(self.state, "chv-a"))
|
||||
|
||||
@@ -324,9 +324,10 @@ class VmConfigOverlayTest(ChvOpt1Case):
|
||||
os.path.join(self.state_dir, "chv-a", "disk.qcow2"),
|
||||
seed_disk=None,
|
||||
data_disk=os.path.join(self.state_dir, "chv-a",
|
||||
"data.qcow2"))
|
||||
"data.raw"))
|
||||
self.assertEqual(cfg["disks"][0]["image_type"], "Qcow2")
|
||||
self.assertEqual(cfg["disks"][1]["image_type"], "Qcow2")
|
||||
# 数据盘是 raw 稀疏盘(2026-11-07:不用 qcow2,零外部依赖)。
|
||||
self.assertEqual(cfg["disks"][1]["image_type"], "Raw")
|
||||
self.assertEqual(len(cfg["disks"]), 2)
|
||||
|
||||
def test_raw_disks_stay_raw(self):
|
||||
@@ -466,22 +467,10 @@ class PersistentPoolTest(ChvOpt1Case):
|
||||
fh.write(b"QFI\xfb fake")
|
||||
return path
|
||||
|
||||
def fake_data(state_dir, vm_name, size_gb=10.0, runner=None):
|
||||
path = os.path.join(state_dir, vm_name, "data.qcow2")
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
if not os.path.exists(path):
|
||||
with open(path, "wb") as fh:
|
||||
fh.write(b"QFI\xfb fake-data")
|
||||
return path
|
||||
|
||||
p1 = mock.patch("cloud.chv.executor.provision_task_disk",
|
||||
side_effect=fake_provision)
|
||||
p2 = mock.patch("cloud.chv.executor.provision_data_disk",
|
||||
side_effect=fake_data)
|
||||
p1.start()
|
||||
p2.start()
|
||||
self.addCleanup(p1.stop)
|
||||
self.addCleanup(p2.stop)
|
||||
return ex
|
||||
|
||||
def _socket(self, vm):
|
||||
@@ -598,7 +587,7 @@ class PersistentPoolTest(ChvOpt1Case):
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.state_dir, "chv-persist-proj1", "disk.qcow2")))
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.state_dir, "chv-persist-proj1", "data.qcow2")))
|
||||
self.state_dir, "chv-persist-proj1", "data.raw")))
|
||||
|
||||
def test_dispatch_persistent_cold_when_pool_empty(self):
|
||||
ex = self.make_exec(
|
||||
@@ -638,7 +627,49 @@ class PersistentPoolTest(ChvOpt1Case):
|
||||
self.assertIn("data", out["deleted_disks"])
|
||||
self.assertFalse(os.path.exists(overlay))
|
||||
self.assertFalse(os.path.exists(os.path.join(
|
||||
self.state_dir, "chv-persist-proj1", "data.qcow2")))
|
||||
self.state_dir, "chv-persist-proj1", "data.raw")))
|
||||
self.assertIsNone(read_persistent_vm(self.state_dir, "proj1"))
|
||||
|
||||
def test_copy_mode_provisions_data_disk_and_tenant(self):
|
||||
# 数据盘与 tenant 标记与 overlay 解耦(2026-11-07:CHV v54
|
||||
# 拒绝一切带 backing file 的 qcow2,持久 VM 在 copy 模式
|
||||
# 下也要有数据盘与归属标记)。
|
||||
ex = self.make_exec(env_extra={"CHV_DISK_MODE": "copy"})
|
||||
self._patch_overlay_provision(ex)
|
||||
disk = ex._provision_task_disk(self._task())
|
||||
self.assertEqual(disk, os.path.join(
|
||||
self.state_dir, "chv-abc123", "disk.raw"))
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.state_dir, "chv-abc123", "data.raw")))
|
||||
self.assertEqual(ex._provision_data_disk_path(self._task()),
|
||||
os.path.join(self.state_dir, "chv-abc123",
|
||||
"data.raw"))
|
||||
from cloud.chv.disk import read_vm_tenant
|
||||
self.assertEqual(read_vm_tenant(self.state_dir, "chv-abc123"),
|
||||
"proj1")
|
||||
|
||||
def test_stop_keeps_protected_copy_disk(self):
|
||||
# 保护不分模式:copy 持久盘 stop/GC 同样不删,显式回收才删。
|
||||
ex = self.make_exec(
|
||||
env_extra={"CHV_WARM_POOL_SIZE": "1",
|
||||
"CHV_DISK_MODE": "copy"})
|
||||
self._patch_overlay_provision(ex)
|
||||
ex.prewarm_persistent_vm("proj1")
|
||||
self._socket("chv-persist-proj1")
|
||||
raw = os.path.join(self.state_dir, "chv-persist-proj1",
|
||||
"disk.raw")
|
||||
self.assertTrue(os.path.exists(raw))
|
||||
self.assertTrue(ex.stop_vm({"id": "chv-persist-proj1"}))
|
||||
self.assertTrue(os.path.exists(raw))
|
||||
self.assertEqual(ex.gc_stale_disks(), {"deleted": [],
|
||||
"failed": []})
|
||||
self.assertTrue(os.path.exists(raw))
|
||||
out = ex.recycle_persistent_vm({"id": "chv-persist-proj1"},
|
||||
tenant_id="proj1")
|
||||
self.assertEqual(out["state"], "recycled")
|
||||
self.assertIn("task", out["deleted_disks"])
|
||||
self.assertIn("data", out["deleted_disks"])
|
||||
self.assertFalse(os.path.exists(raw))
|
||||
self.assertIsNone(read_persistent_vm(self.state_dir, "proj1"))
|
||||
|
||||
def test_refill_warm_pool_noop_when_warm(self):
|
||||
|
||||
Reference in new issue
Block a user