堆叠于 #246(feat-chv-secfix),只到 PR OPEN,未合并、未部署。 安全语义不变:egress、key seed 投递、磁盘删除、默认 provisioned、 tap 校验一律保留;前端零改动。 - WarmPool 定案为实验性(ADR-5):dispatch 永远冷启动, wired_to_dispatch 恒 False,acquire/resume 只保留实验接口, worker 仍调 reconcile_orphans 清理残留。 - 长函数提取:preflight 126→31、wake_vm 135→36、__init__ 69→31、 build_chv_config 159→58;错误文案逐字不变。 - 严格测试替身:fake API 校验 VmConfig(任务盘+seed 盘、net 无重复),fake SSH 校验 known_hosts 钉死;新增 5 项测试。 - Docker 同步守卫:8 个真实标记用例下 Docker 内联版与正典 parse_goal_result 状态全等,改分叉即 fail。 - 配置统一校验:preflight 以 config_invalid 统一兜底端口/配额/ 超时/快照/host-key 模式(构造时已校验正常值,preflight 兜底 事后改写);CHV_NET 与 CHV_NET_JSON 双设仍警告不硬拒。 - 类型与 lint:chv 叶子模块公开 API 补类型注解;stdlib 扫描 揪出 executor 内 1 个真死 import(host_key_paths)并删除。 - ADR:docs/chv-architecture.md(ADR-1 至 ADR-7)。 测试:CHV 163 项全过;cloud 全量 1593 项三跑结果为 8 errors (首跑详细日志因 tail 截断丢失)、全过、1 失败(浏览器代理 时序测试,与 CHV 无关),全量本身存在时序偶发,CHV 套件 确定性全过。
123 lines
4.8 KiB
Python
123 lines
4.8 KiB
Python
"""Cloud Hypervisor REST API 传输(叶子模块,不依赖
|
||
cloud.sandbox,避免循环导入)。
|
||
|
||
只用标准库 raw HTTP over Unix socket,无第三方依赖。
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
import socket
|
||
|
||
|
||
def _unix_socket_api(socket_path, method, path, payload=None):
|
||
"""Cloud Hypervisor REST API 的默认传输:标准库 raw HTTP
|
||
over Unix socket(无第三方依赖)。返回 (status, body),
|
||
body 能解析 JSON 时为 dict,否则为原文本。socket 不存在
|
||
或连接失败时 raise RuntimeError(调用方据此判 api 超时
|
||
/失败,不编造 200)。"""
|
||
if not os.path.exists(socket_path):
|
||
raise RuntimeError("api socket 不存在:%s" % socket_path)
|
||
body = b""
|
||
if payload is not None:
|
||
body = json.dumps(payload).encode("utf-8")
|
||
req_lines = ["%s /api/v1%s HTTP/1.1" % (method, path),
|
||
"Host: localhost",
|
||
"Accept: application/json",
|
||
"Content-Type: application/json",
|
||
"Content-Length: %d" % len(body),
|
||
"Connection: close", "", ""]
|
||
raw = "\r\n".join(req_lines).encode("utf-8") + body
|
||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||
sock.settimeout(5)
|
||
try:
|
||
sock.connect(socket_path)
|
||
sock.sendall(raw)
|
||
chunks = []
|
||
# feat-chv-fix(P1-6):读超时不再当“响应结束”。
|
||
# 先读满响应头,再按 Content-Length 读满体部;超时、
|
||
# 提前关流、体部短于 Content-Length 一律 RuntimeError
|
||
# (调用方判 api_timeout / 传输失败),绝不解析半截
|
||
# 响应。chunked 与无 Content-Length 响应明确拒绝
|
||
# (Cloud Hypervisor REST 不应发 chunked;无长度则
|
||
# 无法证完整性)。
|
||
buf = b""
|
||
header_end = None
|
||
timed_out = False
|
||
while header_end is None:
|
||
try:
|
||
data = sock.recv(65536)
|
||
except socket.timeout:
|
||
timed_out = True
|
||
break
|
||
if not data:
|
||
break
|
||
buf += data
|
||
header_end = buf.find(b"\r\n\r\n")
|
||
if timed_out or header_end is None:
|
||
raise RuntimeError("CHV API 响应头不完整(超时或提前关流)")
|
||
header = buf[:header_end].decode("utf-8", errors="replace")
|
||
body = buf[header_end + 4:]
|
||
content_length = None
|
||
is_chunked = False
|
||
for line in header.split("\r\n")[1:]:
|
||
if ":" not in line:
|
||
continue
|
||
name, value = line.split(":", 1)
|
||
name = name.strip().lower()
|
||
value = value.strip()
|
||
if name == "content-length":
|
||
try:
|
||
content_length = int(value)
|
||
except ValueError:
|
||
raise RuntimeError("CHV API Content-Length 非法")
|
||
elif name == "transfer-encoding" \
|
||
and "chunked" in value.lower():
|
||
is_chunked = True
|
||
if is_chunked:
|
||
raise RuntimeError("CHV API 响应为 chunked,拒绝解析")
|
||
if content_length is None:
|
||
raise RuntimeError("CHV API 响应无 Content-Length,拒绝解析")
|
||
while len(body) < content_length:
|
||
try:
|
||
data = sock.recv(65536)
|
||
except socket.timeout:
|
||
raise RuntimeError(
|
||
"CHV API 响应体截断:已收 %d / 需 %d 字节"
|
||
% (len(body), content_length))
|
||
if not data:
|
||
raise RuntimeError(
|
||
"CHV API 响应体截断:已收 %d / 需 %d 字节"
|
||
% (len(body), content_length))
|
||
body += data
|
||
body = body[:content_length]
|
||
finally:
|
||
sock.close()
|
||
text = header + "\r\n\r\n" + body.decode("utf-8", errors="replace")
|
||
status = 0
|
||
if text.startswith("HTTP/"):
|
||
try:
|
||
status = int(text.split()[1])
|
||
except (IndexError, ValueError):
|
||
status = 0
|
||
body_text = body.decode("utf-8", errors="replace")
|
||
try:
|
||
parsed = json.loads(body_text) if body_text.strip() else {}
|
||
except ValueError:
|
||
parsed = body_text
|
||
return status, parsed
|
||
|
||
|
||
def normalize_api_result(result) -> tuple:
|
||
"""API 调用归一化:fake api_client 可返回 (status, body)
|
||
或 {"status":..., "body":...} 或带 .status 的对象,本
|
||
函数统一成 (status:int, body)。"""
|
||
if isinstance(result, tuple) and len(result) == 2:
|
||
return int(result[0]), result[1]
|
||
if isinstance(result, dict):
|
||
status = result.get("status", result.get("status_code"))
|
||
body = result.get("body", result)
|
||
return (int(status) if status is not None else 0), body
|
||
status = getattr(result, "status", None) \
|
||
or getattr(result, "status_code", None)
|
||
return (int(status) if status is not None else 0), result
|