diff --git a/cloud/chv/executor.py b/cloud/chv/executor.py index 77b35f3..0043496 100644 --- a/cloud/chv/executor.py +++ b/cloud/chv/executor.py @@ -3322,17 +3322,21 @@ class CloudHypervisorExecutor(Executor): result["resume_seconds"] = resume_seconds return result finally: - try: - if self._vm_proc_dead({"id": vm}): - # VM 已死:暂停无意义,清登记,免得下次 - # acquire 把死 VM 当可用预热机。 - if tenant: - clear_persistent_vm(self.state_dir, tenant) - else: - self.suspend_after_task({"id": vm}, - tenant_id=tenant) - except Exception: - pass + self._suspend_persistent_after_task(vm, tenant) + + def _suspend_persistent_after_task(self, vm, tenant): + """持久任务收尾(_run_with_persistent_vm 的 finally 段, + 拆出只为满足 50 行函数上限):VM 已死即清登记(免得 + 下次 acquire 把死 VM 当可用预热机),否则暂停回池; + 收尾失败不掩盖任务本身的结果。""" + try: + if self._vm_proc_dead({"id": vm}): + if tenant: + clear_persistent_vm(self.state_dir, tenant) + else: + self.suspend_after_task({"id": vm}, tenant_id=tenant) + except Exception: + pass def dispatch_task_persistent(self, task: dict, extra_env: dict | None = None, diff --git a/cloud/tests/test_chv_opt_stage1.py b/cloud/tests/test_chv_opt_stage1.py index 8cbb66f..cd3f6cf 100644 --- a/cloud/tests/test_chv_opt_stage1.py +++ b/cloud/tests/test_chv_opt_stage1.py @@ -412,8 +412,11 @@ class CrossTenantRestoreTest(ChvOpt1Case): ex._validate_restore(task_b, url) self.assertEqual(ctx.exception.kind, "snapshot_tenant_mismatch") + # 严格校验默认开启(设计 §2.2):跨租户拒绝走设计事件 + # vm_restore_denied;legacy 的 vm_restore_tenant_denied + # 只在显式关闭严格校验时才出现。 self.assertTrue(any( - e == "vm_restore_tenant_denied" for e, _ in events)) + e == "vm_restore_denied" for e, _ in events)) def test_same_tenant_restore_passes_validation(self): url = self._snap_url("chv-a") @@ -426,7 +429,11 @@ class CrossTenantRestoreTest(ChvOpt1Case): def test_legacy_unmarked_snapshot_warns_but_passes(self): url = self._snap_url("chv-a", "old") self._record_snapshot("chv-a", url, None) - ex = self.make_exec() + # legacy 警告放行只在显式关闭严格校验时保留 + # (CHV_SNAPSHOT_STRICT_TENANT=0;默认开启时未标记 + # 快照即拒绝,见 test_unmarked_snapshot_denied_in_hybrid_mode)。 + ex = self.make_exec(env_extra={ + "CHV_SNAPSHOT_STRICT_TENANT": "0"}) events = [] ex.audit_callback = lambda e, t, d, k: events.append((e, k)) snapshot_url, _net = ex._validate_restore( diff --git a/cloud/tests/test_chv_v3_fixes.py b/cloud/tests/test_chv_v3_fixes.py index 170c6e5..993e1cd 100644 --- a/cloud/tests/test_chv_v3_fixes.py +++ b/cloud/tests/test_chv_v3_fixes.py @@ -38,12 +38,14 @@ from pathlib import Path from unittest import mock try: + from cloud.chv.disk import record_snapshot from cloud.chv.executor import CloudHypervisorExecutor from cloud.chv.hostkeys import write_seed from cloud.chv.ssh_transport import remote_command from cloud.sandbox import ChvFailure except ImportError: sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + from cloud.chv.disk import record_snapshot from cloud.chv.executor import CloudHypervisorExecutor from cloud.chv.hostkeys import write_seed from cloud.chv.ssh_transport import remote_command @@ -195,6 +197,14 @@ class V3Case(unittest.TestCase): fh.write(content) return "file://" + path, path + def _register_snapshot(self, url, tenant="proj1"): + # 严格快照校验(设计 §2.2,默认开启):restore 测试的 + # 快照须登记带 tenant 且与 _task() 的 project_id 一致; + # 本 helper 只登记,不改变测试本身的意图(盘预检 / + # source_url / 清旧 VMM 仍由各自断言覆盖)。 + record_snapshot(self.state_dir, "chv-abc123", url, 20.0, + tenant_id=tenant) + def _spawn_fake_vmm(self, sock_path, argv0="cloud-hypervisor"): """真实 spawn 一个长眠进程,argv[0] 伪装成 VMM, 命令行带 --api-socket (F2/V-3③ 的扫描目标)。""" @@ -223,6 +233,7 @@ class SnapshotV3Test(V3Case): def test_restore_still_passes_source_url(self): url, _ = self._snap_dir() + self._register_snapshot(url) ex = self.make_exec() out = ex.restore_vm(self._task(), url) self.assertEqual(out["mode"], "restored") @@ -248,6 +259,7 @@ class SnapshotV3Test(V3Case): class RestorePrepareV3Test(V3Case): def test_restore_kills_registered_old_vmm_and_socket(self): url, _ = self._snap_dir() + self._register_snapshot(url) ex = self.make_exec() ex.wake_vm(self._task()) old_proc = self.proc @@ -265,6 +277,7 @@ class RestorePrepareV3Test(V3Case): def test_restore_kills_unregistered_vmm_by_socket(self): url, _ = self._snap_dir() + self._register_snapshot(url) ex = self.make_exec() sock = os.path.join(self.state_dir, "chv-abc123.sock") os.makedirs(self.state_dir, exist_ok=True) @@ -314,6 +327,7 @@ class HibernateDiskV3Test(V3Case): "config.json": json.dumps( {"disks": [{"path": missing_disk}]}).encode(), "state.json": b"{}"}) + self._register_snapshot(url) with self.assertRaises(ChvFailure) as ctx: ex.restore_vm(self._task(), url) self.assertEqual(ctx.exception.kind, "snapshot_disk_missing") @@ -327,6 +341,7 @@ class HibernateDiskV3Test(V3Case): "config.json": json.dumps( {"disks": [{"path": disk}]}).encode(), "state.json": b"{}"}) + self._register_snapshot(url) out = ex.restore_vm(self._task(), url) self.assertEqual(out["mode"], "restored") diff --git a/cloud/tests/test_cloud_hypervisor.py b/cloud/tests/test_cloud_hypervisor.py index b34faf2..f00a963 100644 --- a/cloud/tests/test_cloud_hypervisor.py +++ b/cloud/tests/test_cloud_hypervisor.py @@ -20,6 +20,7 @@ from pathlib import Path from unittest import mock try: + from cloud.chv.disk import record_snapshot from cloud.chv.experimental_pool import WarmPool from cloud.sandbox import ( ChvFailure, @@ -28,6 +29,7 @@ try: ) except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + from cloud.chv.disk import record_snapshot from cloud.chv.experimental_pool import WarmPool from cloud.sandbox import ( ChvFailure, @@ -406,6 +408,10 @@ class LifecycleTest(ChvCase): # 路径 + 声明快照目录(生产默认 provisioned + 白名单 # 目录,旧 file:///snap/1 不再直通)。 ex = self.make_exec(env_extra={"CHV_SNAPSHOT_DIR": "/snap"}) + # 严格快照校验(设计 §2.2,默认开启):恢复须登记带 + # tenant 且与 _task() 的 project_id(proj1)一致。 + record_snapshot(self.state_dir, "chv-abc123", + "file:///snap/1", 20.0, tenant_id="proj1") out = ex.restore_vm(self._task(), "file:///snap/1") self.assertEqual(out["mode"], "restored") self.assertTrue(out["ready"])