架构:WarmPool 从 cloud/chv/pool.py 移到 experimental_pool.py, 生产残留清理彻底分文件到 cloud/chv/orphans.py(facade 直接读 registry 文件清理,不构造池、不碰 prewarm/acquire/release); cloud/chv 与 cloud/sandbox 不再导出 WarmPool,旧路径一律 ImportError(CHV 未合并、无生产用户,兼容保留已无保留理由); worker 改走 orphans facade,不再持有 _warm_pool;#215 对齐从 接口形状推进到 REPLACEMENT_POINTS 替换清单(R1/R2 可机械替换、 R3 永不替换,#215 模块当前不存在由测试钉住)。 可维护性:build_chv_config 拆成 base/ssh/network/quota/disk/ timeout 六片段(组装只做 dict 合并,键冲突即报错;38 字段集 与取值不变,单项非法报错文案逐字一致);ADR-12 模块职责图 与禁 import 清单(AST 钉住);cloud/chv/ 公开 API 全量类型 标注、无 Any 逃逸,并修正三处旧标注错误;死代码扫一遍 (删实验池已无调用的 _quarantine 委托与未用 _LOGGER)。 前端(只读侧,不做控制按钮):chv.ts 新增纯逻辑谓词与文案 (执行器精确匹配、附件守卫、VM 状态文案、列表徽标);列表 行新增 VM 已报告死亡徽标;详情对全部 CHV 任务显示 VM 行 (已报告死亡/未报告死亡,不猜 live);WorkersSection 与成本 卡沿用已有数据,不新开池管理/成本页;服务端未新增第二个 生命周期字段(持久化事实只有三项,chv_vm_dead 已是完整派生), 只把派生抽成 _chv_vm_dead_from_row 并补 goal 回落形态测试。
144 lines
5.2 KiB
Python
144 lines
5.2 KiB
Python
"""Cloud Hypervisor REST API 传输(叶子模块,不依赖
|
||
cloud.sandbox,避免循环导入)。
|
||
|
||
只用标准库 raw HTTP over Unix socket,无第三方依赖。
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
import socket
|
||
|
||
|
||
def _read_header(sock):
|
||
"""读满响应头:返回 (header_text, 已收体部前缀)。
|
||
|
||
超时或提前关流即 RuntimeError,绝不把半截头当结束
|
||
(feat-chv-fix P1-6 的语义,纯移动)。"""
|
||
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")
|
||
return header, buf[header_end + 4:]
|
||
|
||
|
||
def _parse_content_length(header):
|
||
"""从响应头取 Content-Length:chunked 或无长度即拒绝。
|
||
|
||
chunked 明确拒绝(CHV REST 不应发 chunked);无长度则
|
||
无法证完整性,同样拒绝。"""
|
||
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,拒绝解析")
|
||
return content_length
|
||
|
||
|
||
def _read_body(sock, body, content_length):
|
||
"""按 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
|
||
return body[:content_length]
|
||
|
||
|
||
def _parse_response(header, body):
|
||
"""从已读满的头与体部解析 (status, body):能解析 JSON
|
||
时为 dict,否则为原文本;状态行解析失败时 status 为 0。"""
|
||
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 _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)。读满语义见 _read_header /
|
||
_parse_content_length / _read_body(P1-6,纯移动)。"""
|
||
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)
|
||
header, body = _read_header(sock)
|
||
content_length = _parse_content_length(header)
|
||
body = _read_body(sock, body, content_length)
|
||
finally:
|
||
sock.close()
|
||
return _parse_response(header, body)
|
||
|
||
|
||
def normalize_api_result(result: dict) -> 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
|