- 拆模块:CHV 从 cloud/sandbox.py 巨类拆到 cloud/chv/(executor/ pool/config/api_client/ssh_transport/disk/errors),sandbox 只留 兼容 re-export,旧导入路径与新路径是同一对象 - 去重复:goal 解析、host 顶层参数、runner 超时抽成共享 helper, Local 与 CHV 在矩阵输入下结论全等(Docker goal-resume 变体 有意保留内联版,见 PR 描述) - 安全:SSH host key 按 VM 钉死(state_dir/<vm>/known_hosts, ssh-keyscan 首次捕获 TOFU),废除 no + /dev/null;CHV_SSH_USER 空串与 CHV_SSH_PORT 越界直接拒绝,不静默 fallback - execute 语义:只读已运行 VM,未运行报 vm_not_running,不为 status 起 VM - 预热池:registry 落盘、重启载入只当孤儿、reconcile 清理、 损坏 quarantine;条目仍一次性,绝不复用脏 VM - 长任务:VM 中途死亡判 interrupted(断点可续跑);中途心跳 经评估不做(无客内进度信号可采) - #215 未合并:只列复用清单,不复制模块 测试:原 42 项 + 新 24 项全过;相关旧套件与 cloud 全量 1496 项 全过(skipped=4)。验证仍是 mock + 干跑 + 单元测试,未做真机、 压测、安全审计;堆叠于 #216 之上,base 为 feat-cloud-hypervisor。
75 lines
2.7 KiB
Python
75 lines
2.7 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 = []
|
||
while True:
|
||
try:
|
||
data = sock.recv(65536)
|
||
except socket.timeout:
|
||
break
|
||
if not data:
|
||
break
|
||
chunks.append(data)
|
||
finally:
|
||
sock.close()
|
||
text = b"".join(chunks).decode("utf-8", errors="replace")
|
||
status = 0
|
||
if text.startswith("HTTP/"):
|
||
try:
|
||
status = int(text.split()[1])
|
||
except (IndexError, ValueError):
|
||
status = 0
|
||
body_text = text.split("\r\n\r\n", 1)[1] if "\r\n\r\n" in text \
|
||
else ""
|
||
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
|