P0:seed 磁盘 CIDATA ISO 挂进 VmConfig 第二块只读盘; restore provisioned 下复用钉死 known_hosts、缺失 fail-fast 绝不 keyscan;preflight 拦截 CHV_DISK_MODE=reuse(显式 实验二次确认 + 审计放行);SSH 目标选客机 IP 方案(显式 host > net 静态 ip > 显式转发宣称回环,三者皆无 fail-fast)。 P1:网关 extra_env 经 SSH env 前缀注入、worker 守卫收窄到 Local/CHV;取消/清理/看门狗接 CHV 通用路径;WarmPool acquire 内 resume;容量门持久化计数;磁盘配额总量+余量 与已存在磁盘校验重拷;API 按 Content-Length 验完整性; 快照 file:// 验落盘再 shutdown;execute 死端口归类 vm_not_running。 P2 全部排除未做(用户范围收窄令,留待以后一轨)。 测试:原 84 + 新增 29 = 113 项,mock,无真 VM。
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):
|
||
"""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
|