- attach_tenant_data_disk:PUT /vm.add-disk body 由 {"path"} 改为
{"path", "image_type"}(复用 _disk_image_type,与 create 时盘
同款;租户数据盘 raw → Raw)。CHV v54 已移除盘格式自动探测,
只发 path 时真机回 400(p2 复测);证据:v54 release notes
“autodetection removed”、ch-remote v54.0 add-disk --help 含
image_type=<raw,qcow2,...>、本仓 _vm_config_disks 的 vm.boot
400 "Image type required for disk" 真机实测(2026-11-07)。
- 非 2xx 时 body 原样记入 data_disk_attach_failed 审计事件与
ChvFailure detail(新增 _api_error_body_text,截断上限 500
字符:CHV 400 体通常一行 JSON <200 字符,500 足够装完整原话;
超长截断并标 "…(已截断)",保审计与异常串有界);同类问题一并
修 _create_vm(vm_create_failed 带 body)。fail-closed 语义
不变(失败仍销毁/回落,不把未挂盘的 VM 当已绑定)。
- 单测:attach 成功断言 image_type=Raw;新增 400 body 记录用例
与超长截断用例;vm.create 失败 body 用例。
- 真机探针:chv-multi-host/probe-add-disk.py(px2,旧形状 A/B +
产品函数成功路径 + 两条 400 记录路径,待复测代理跑)。
全量:Ran 4678,OK (skipped=14),0 失败/错误。
This commit is contained in:
1 parent
6c03ac9b0f
commit
0dc8328ecc
3 files changed
+101
-8
No files matched your search
+51
-7
@@ -313,6 +313,37 @@ def _disk_image_type(path: str) -> str:
|
||||
return "Qcow2" if str(path).endswith(".qcow2") else "Raw"
|
||||
|
||||
|
||||
# CHV API 错误体截断上限(p2 修复,见 _api_error_body_text):
|
||||
# 审计事件与 ChvFailure detail 里只记前 500 字符。取 500 的
|
||||
# 理由:CHV 的 400 体通常是一行 JSON {"error": "..."}(真机
|
||||
# 实测 <200 字符),500 足够装下完整原话;再长基本是重复字段
|
||||
# 或超大体,截断可保审计事件与异常串有界(审计 spill 文件按
|
||||
# 1MB 轮转;异常串还会进 warm_pool_acquire_failed 的 200 字符
|
||||
# 二次截断,见 _resume_clean_pool)。
|
||||
CHV_API_ERROR_BODY_LIMIT = 500
|
||||
|
||||
|
||||
def _api_error_body_text(body) -> str:
|
||||
"""CHV API 错误响应体转可记文本(p2 修复:非 2xx 时不再
|
||||
丢弃 body,见 attach_tenant_data_disk / _create_vm):
|
||||
dict/list 压成一行 JSON,其余 str();超
|
||||
CHV_API_ERROR_BODY_LIMIT 字符截断并标明(超出部分丢弃,
|
||||
只留前 500 字符的可定位信息)。body 本身是 CHV 的错误
|
||||
文本(只含盘路径/状态,不含密钥),可进审计与异常。"""
|
||||
if isinstance(body, (dict, list)):
|
||||
try:
|
||||
text = json.dumps(body, ensure_ascii=False, default=str)
|
||||
except Exception:
|
||||
text = str(body)
|
||||
elif isinstance(body, str):
|
||||
text = body
|
||||
else:
|
||||
text = str(body)
|
||||
if len(text) > CHV_API_ERROR_BODY_LIMIT:
|
||||
return text[:CHV_API_ERROR_BODY_LIMIT] + "…(已截断)"
|
||||
return text
|
||||
|
||||
|
||||
def _default_popen_launcher(cmd):
|
||||
"""默认 VMM 启动器:独立会话子进程,输出丢弃(纯移动)。"""
|
||||
return subprocess.Popen(
|
||||
@@ -2072,16 +2103,19 @@ class CloudHypervisorExecutor(Executor):
|
||||
+ 可选 seed 盘 + 可选数据盘;attach_data_disk=False 时
|
||||
不挂数据盘(干净池出生)。失败即 ChvFailure。"""
|
||||
socket_path = self.api_socket(task)
|
||||
status, _body = self._api_call(
|
||||
status, body = self._api_call(
|
||||
socket_path, "PUT", "/vm.create",
|
||||
self.build_vm_config(
|
||||
task_disk, seed_disk=seed_disk,
|
||||
data_disk=self._provision_data_disk_path(task)
|
||||
if attach_data_disk else None))
|
||||
if status not in (200, 204):
|
||||
# p2 修复(同类问题,见 attach_tenant_data_disk):
|
||||
# vm.create 失败时 body 原样记入异常,不再丢弃。
|
||||
raise ChvFailure(
|
||||
"vm_create_failed",
|
||||
"vm.create 失败:status=%s" % status)
|
||||
"vm.create 失败:status=%s body=%s"
|
||||
% (status, _api_error_body_text(body)))
|
||||
self._emit("vm_create", task,
|
||||
"vm=%s disk=%s" % (name, task_disk))
|
||||
|
||||
@@ -3795,7 +3829,12 @@ class CloudHypervisorExecutor(Executor):
|
||||
/home/chv/workspace)→ 写 VM 归属标记。任一步失败即
|
||||
ChvFailure(fail-closed,调用方回落冷启或拒绝,不把
|
||||
未挂盘的 VM 当已绑定发给任务)。返回 {vm, tenant_id,
|
||||
data_disk}。"""
|
||||
data_disk}。热挂载 body 与 create 时盘同款:path +
|
||||
image_type(CHV v54 已移除盘格式自动探测,只发 path
|
||||
时真机回 400,见 p2 复测);非 2xx 时 CHV 的 body
|
||||
原样记入 data_disk_attach_failed 审计事件与异常
|
||||
detail(超 500 字符截断,见 _api_error_body_text),
|
||||
不再丢弃。"""
|
||||
tenant = self._tenant_id(task)
|
||||
if not tenant:
|
||||
raise ChvFailure(
|
||||
@@ -3803,14 +3842,19 @@ class CloudHypervisorExecutor(Executor):
|
||||
"挂载数据盘须带 tenant_id:无归属的任务不能绑盘")
|
||||
disk = data_disk or self._provision_tenant_data_disk(tenant)
|
||||
self._verify_task_disk_source(vm, tenant, disk)
|
||||
status, _body = self._api_call(
|
||||
status, body = self._api_call(
|
||||
self.api_socket({"id": vm}), "PUT", "/vm.add-disk",
|
||||
{"path": disk})
|
||||
{"path": disk, "image_type": _disk_image_type(disk)})
|
||||
if status not in (200, 204):
|
||||
body_text = _api_error_body_text(body)
|
||||
self._emit("data_disk_attach_failed", task,
|
||||
"vm=%s status=%s body=%s"
|
||||
% (vm, status, body_text),
|
||||
"data_disk_attach_failed")
|
||||
raise ChvFailure(
|
||||
"data_disk_attach_failed",
|
||||
"数据盘热挂载失败:vm=%s status=%s"
|
||||
% (vm, status))
|
||||
"数据盘热挂载失败:vm=%s status=%s body=%s"
|
||||
% (vm, status, body_text))
|
||||
result = self._call_runner(self.build_ssh_command(
|
||||
{"id": vm}, ["/usr/local/bin/chv-data-mount"]))
|
||||
if getattr(result, "returncode", 1) != 0:
|
||||
|
||||
@@ -195,12 +195,14 @@ class WakeFailureTest(CloseupCase):
|
||||
def api(socket_path, method, path, payload=None):
|
||||
self.api_calls.append((method, path))
|
||||
if method == "PUT" and path == "/vm.create":
|
||||
return (500, {})
|
||||
return (500, {"error": "boom-create"})
|
||||
return (200, {})
|
||||
ex = self.make_exec(api_client=api)
|
||||
with self.assertRaises(ChvFailure) as ctx:
|
||||
ex.wake_vm(self._task())
|
||||
self.assertEqual(ctx.exception.kind, "vm_create_failed")
|
||||
# p2 同类修复:vm.create 失败的 body 也须进异常 detail。
|
||||
self.assertIn("boom-create", str(ctx.exception))
|
||||
self.assertIn("vm_failed", self._audit_events())
|
||||
self.assertEqual(ex._vms, {})
|
||||
self.assertTrue(any(c[-1] == "shutdown" for c in self.calls))
|
||||
|
||||
@@ -489,6 +489,9 @@ class DataDiskAttachTest(HybridACase):
|
||||
(m, p, pl) for m, p, pl in self.api_calls
|
||||
if p == "/vm.add-disk")
|
||||
self.assertEqual(payload["path"], out["data_disk"])
|
||||
# CHV v54 已移除盘格式自动探测:热插盘 body 须与 create
|
||||
# 时盘同款带 image_type(租户数据盘是 raw 稀疏盘 → Raw)。
|
||||
self.assertEqual(payload["image_type"], "Raw")
|
||||
# 客内挂载脚本被调过(chv-data-mount)。
|
||||
self.assertTrue(any(
|
||||
"/usr/local/bin/chv-data-mount" in call
|
||||
@@ -496,6 +499,50 @@ class DataDiskAttachTest(HybridACase):
|
||||
self.assertEqual(
|
||||
read_vm_tenant(self.state_dir, "chv-pool-0"), "tenant-a")
|
||||
|
||||
def test_attach_failure_records_chv_body(self):
|
||||
# p2 修复:非 2xx 时 CHV 的原话(body)须进异常 detail
|
||||
# 与审计事件,不再丢弃;失败仍 fail-closed(不挂载、不
|
||||
# 写归属标记)。
|
||||
def api(socket_path, method, path, payload=None):
|
||||
self.api_calls.append((method, path, payload))
|
||||
if path == "/vm.add-disk":
|
||||
return (400, {"error": "Image type required for disk"})
|
||||
return (200, {})
|
||||
ex = self.make_exec(api_client=api)
|
||||
with self.assertRaises(ChvFailure) as ctx:
|
||||
ex.attach_tenant_data_disk(
|
||||
self._task(project="tenant-a"), "chv-pool-0")
|
||||
self.assertEqual(ctx.exception.kind, "data_disk_attach_failed")
|
||||
self.assertIn("Image type required for disk", str(ctx.exception))
|
||||
self.assertIn("status=400", str(ctx.exception))
|
||||
details = self._event_details("data_disk_attach_failed")
|
||||
self.assertEqual(len(details), 1)
|
||||
self.assertIn("Image type required for disk", details[0])
|
||||
self.assertIn("status=400", details[0])
|
||||
# 挂载脚本没跑、归属标记没写。
|
||||
self.assertFalse(any(
|
||||
"/usr/local/bin/chv-data-mount" in call
|
||||
for call in self.calls if call and call[0] == "ssh"))
|
||||
self.assertIsNone(read_vm_tenant(self.state_dir, "chv-pool-0"))
|
||||
|
||||
def test_attach_failure_truncates_long_body(self):
|
||||
# 超 500 字符的 body 只记前 500 字符并标明,不把整段
|
||||
# 塞进审计与异常。
|
||||
def api(socket_path, method, path, payload=None):
|
||||
self.api_calls.append((method, path, payload))
|
||||
if path == "/vm.add-disk":
|
||||
return (400, {"error": "E" * 600})
|
||||
return (200, {})
|
||||
ex = self.make_exec(api_client=api)
|
||||
with self.assertRaises(ChvFailure) as ctx:
|
||||
ex.attach_tenant_data_disk(
|
||||
self._task(project="tenant-a"), "chv-pool-0")
|
||||
self.assertIn("…(已截断)", str(ctx.exception))
|
||||
self.assertNotIn("E" * 600, str(ctx.exception))
|
||||
details = self._event_details("data_disk_attach_failed")
|
||||
self.assertEqual(len(details), 1)
|
||||
self.assertIn("…(已截断)", details[0])
|
||||
|
||||
def test_dispatch_pool_hit_binds_tenant_disk_and_destroys_vm(self):
|
||||
ex = self.make_exec()
|
||||
ex.prewarm_clean_pool(slot=0)
|
||||
|
||||
Reference in new issue
Block a user