236 lines
10 KiB
Python
236 lines
10 KiB
Python
"""Cloud Hypervisor 执行器配置解析(叶子模块,不依赖
|
||
cloud.sandbox,避免循环导入)。
|
||
|
||
职责只有两件:内存字符串解析,以及把构造参数 + 环境变量
|
||
解析成一份显式配置。参数优先于环境变量、环境变量优先于
|
||
默认值——与 #216 原 __init__ 的 `or` 链完全一致,只收紧了
|
||
两处原来会静默 fallback 的 nonsense 输入(见下文),收紧点
|
||
在 PR 描述里明写、单测钉住:
|
||
|
||
- CHV_SSH_USER 显式传空字符串:原来 `or` 链会静默 fallback
|
||
成 root(调用方以为没给用户、实际跑了 root),现在直接
|
||
ValueError。
|
||
- CHV_SSH_PORT 显式传 0、或解析后超出 1-65535:原来 0 会
|
||
静默 fallback 成 2222、其它越界值会原样进 ssh 命令,现在
|
||
直接 ValueError。
|
||
|
||
与 #215 的关系(只列清单,不复制模块):#215 未合并,
|
||
本轨不假装复用它。#215 合并后应再复用的点:超时类
|
||
(api_timeout/ready_timeout/ready_interval/task_timeout →
|
||
timeouts 模块)、门禁类(preflight fail-fast、容量门、磁盘
|
||
配额门、egress fail-closed → gates 模块)、配额类
|
||
(max_vms、disk_gb、任务成本走 worker quota → quota 模块)、
|
||
隔离类(单任务单 VM、磁盘 copy 模式、egress 代理、SSH 钉死
|
||
→ sandbox_policy 模块)。见 PR 描述的复用清单。
|
||
"""
|
||
|
||
import os
|
||
import re
|
||
|
||
|
||
def _parse_memory_bytes(value):
|
||
"""内存字符串转字节:2G/512M/1024K/2048(无单位=字节)。
|
||
非法值 ValueError,绝不静默按默认算。"""
|
||
m = re.match(r"^\s*(\d+)\s*([KMGkmg]?)\s*[Bb]?\s*$",
|
||
str(value))
|
||
if not m:
|
||
raise ValueError("CHV_MEM 格式非法:%r" % (value,))
|
||
mult = {"": 1, "K": 1024, "M": 1024 ** 2, "G": 1024 ** 3}
|
||
return int(m.group(1)) * mult[m.group(2).upper()]
|
||
|
||
|
||
class ChvConfig:
|
||
"""解析后的 CHV 配置:纯数据对象,字段名与
|
||
CloudHypervisorExecutor 原 __init__ 的属性名一一对应,
|
||
执行器 __init__ 直接把字段抄到 self 上,旧测试读属性
|
||
的方式全部不变。"""
|
||
|
||
def __init__(self, **fields):
|
||
for key, value in fields.items():
|
||
setattr(self, key, value)
|
||
|
||
|
||
|
||
def _ssh_fragment(params: dict, env: dict) -> dict:
|
||
"""SSH/命令面配置片段:返回 dict,供 build_chv_config 组装。
|
||
错误文案与原内联代码逐字一致,校验时机不变(构造时)。"""
|
||
ssh_key = params.get("ssh_key") or env.get("CHV_SSH_KEY")
|
||
if params.get("ssh_user") is not None \
|
||
and params.get("ssh_user") == "":
|
||
raise ValueError("CHV_SSH_USER 不能为空字符串")
|
||
ssh_user = params.get("ssh_user") or env.get("CHV_SSH_USER") \
|
||
or "root"
|
||
if params.get("ssh_port") is not None:
|
||
raw_port = params.get("ssh_port")
|
||
else:
|
||
raw_port = env.get("CHV_SSH_PORT") or 2222
|
||
try:
|
||
ssh_port = int(raw_port)
|
||
except (TypeError, ValueError):
|
||
raise ValueError("CHV_SSH_PORT 必须是整数")
|
||
if not 1 <= ssh_port <= 65535:
|
||
raise ValueError("CHV_SSH_PORT 必须在 1-65535 之间")
|
||
host_key_mode = params.get("host_key_mode") \
|
||
or env.get("CHV_HOST_KEY_MODE") or "provisioned"
|
||
if host_key_mode not in ("provisioned", "keyscan"):
|
||
raise ValueError(
|
||
"CHV_HOST_KEY_MODE 只能是 provisioned 或 keyscan:%s"
|
||
% host_key_mode)
|
||
return {
|
||
"ssh_key": ssh_key, "ssh_user": ssh_user,
|
||
"ssh_port": ssh_port, "host_key_mode": host_key_mode,
|
||
"guest_host_py": params.get("guest_host_py") or "/app/host.py",
|
||
"cmdline": params.get("cmdline") or env.get("CHV_CMDLINE")
|
||
or "console=hvc0 root=/dev/vda1 rw",
|
||
"net": params.get("net") or env.get("CHV_NET"),
|
||
"net_json": params.get("net_json") or env.get("CHV_NET_JSON"),
|
||
"ssh_host": params.get("ssh_host") or env.get("CHV_SSH_HOST"),
|
||
"ssh_forward": env.get("CHV_SSH_FORWARD") == "1",
|
||
}
|
||
|
||
|
||
def _quota_fragment(env: dict) -> dict:
|
||
"""配额/磁盘/快照配置片段:返回 dict,错误文案逐字一致。"""
|
||
try:
|
||
max_vms = int(env.get("CHV_MAX_VMS") or 4)
|
||
except ValueError:
|
||
raise ValueError("CHV_MAX_VMS 必须是整数")
|
||
disk_mode = env.get("CHV_DISK_MODE") or "copy"
|
||
if disk_mode not in ("copy", "reuse"):
|
||
raise ValueError(
|
||
"CHV_DISK_MODE 只能是 copy 或 reuse:%s" % disk_mode)
|
||
try:
|
||
disk_gb = float(env.get("CHV_DISK_GB")) \
|
||
if env.get("CHV_DISK_GB") else 50.0
|
||
except ValueError:
|
||
raise ValueError("CHV_DISK_GB 必须是数字")
|
||
if disk_gb <= 0:
|
||
raise ValueError("CHV_DISK_GB 必须大于 0")
|
||
try:
|
||
snapshot_max_count = int(env.get("CHV_SNAPSHOT_MAX_COUNT")
|
||
or 10)
|
||
snapshot_max_total_gb = float(
|
||
env.get("CHV_SNAPSHOT_MAX_TOTAL_GB") or 20.0)
|
||
snapshot_retention_days = float(
|
||
env.get("CHV_SNAPSHOT_RETENTION_DAYS") or 30.0)
|
||
except ValueError:
|
||
raise ValueError("CHV_SNAPSHOT_* 必须是数字")
|
||
if snapshot_max_count < 0 or snapshot_max_total_gb < 0 \
|
||
or snapshot_retention_days < 0:
|
||
raise ValueError("CHV_SNAPSHOT_* 不能为负数")
|
||
return {
|
||
"max_vms": max_vms, "disk_mode": disk_mode,
|
||
"disk_gb": disk_gb,
|
||
"reuse_experiment": env.get(
|
||
"CHV_REUSE_SINGLE_TENANT_EXPERIMENT") == "1",
|
||
"single_tenant_project": env.get("CHV_SINGLE_TENANT_PROJECT"),
|
||
"snapshot_max_count": snapshot_max_count,
|
||
"snapshot_max_total_gb": snapshot_max_total_gb,
|
||
"snapshot_retention_days": snapshot_retention_days,
|
||
"snapshot_dir": env.get("CHV_SNAPSHOT_DIR"),
|
||
"allow_remote_snapshot": env.get("CHV_ALLOW_REMOTE_SNAPSHOT")
|
||
== "1",
|
||
"egress_proxy": env.get("CHV_EGRESS_PROXY"),
|
||
"allow_direct_egress": env.get("CHV_ALLOW_DIRECT_EGRESS")
|
||
== "1",
|
||
}
|
||
|
||
|
||
def _timeout_fragment(params: dict, env: dict) -> dict:
|
||
"""超时/池/快照 URL 配置片段:返回 dict,错误文案逐字一致。"""
|
||
task_timeout_raw = env.get("CHV_TASK_TIMEOUT")
|
||
task_timeout = float(task_timeout_raw) \
|
||
if task_timeout_raw else None
|
||
try:
|
||
warm_pool_size = int(env.get("CHV_WARM_POOL_SIZE") or 0)
|
||
except ValueError:
|
||
raise ValueError("CHV_WARM_POOL_SIZE 必须是整数")
|
||
try:
|
||
api_timeout = float(
|
||
params.get("api_timeout")
|
||
if params.get("api_timeout") is not None
|
||
else env.get("CHV_API_TIMEOUT") or 30)
|
||
ready_timeout = float(
|
||
params.get("ready_timeout")
|
||
if params.get("ready_timeout") is not None
|
||
else env.get("CHV_READY_TIMEOUT") or 120)
|
||
ready_interval = float(
|
||
params.get("ready_interval")
|
||
if params.get("ready_interval") is not None
|
||
else env.get("CHV_READY_INTERVAL") or 5)
|
||
except ValueError:
|
||
raise ValueError("CHV_*_TIMEOUT/INTERVAL 必须是数字")
|
||
if ready_interval <= 0:
|
||
ready_interval = 5
|
||
return {
|
||
"task_timeout": task_timeout, "warm_pool_size": warm_pool_size,
|
||
"api_timeout": api_timeout, "ready_timeout": ready_timeout,
|
||
"ready_interval": ready_interval,
|
||
"snapshot_url": env.get("CHV_SNAPSHOT_URL"),
|
||
}
|
||
|
||
|
||
def _assemble_chv_config(base: dict, ssh: dict, quota: dict,
|
||
timeouts: dict):
|
||
"""把基础字段 + SSH/配额/超时三个片段组装成 ChvConfig。
|
||
|
||
纯移动:字段名与取值逐字不变,build_chv_config 只解析。"""
|
||
return ChvConfig(
|
||
binary=base["binary"], kernel=base["kernel"],
|
||
rootfs=base["rootfs"], cpus=base["cpus"],
|
||
memory=base["memory"], state_dir=base["state_dir"],
|
||
ssh_key=ssh["ssh_key"], ssh_user=ssh["ssh_user"],
|
||
ssh_port=ssh["ssh_port"],
|
||
guest_host_py=ssh["guest_host_py"], cmdline=ssh["cmdline"],
|
||
net=ssh["net"], net_json=ssh["net_json"],
|
||
kvm_path=base["kvm_path"], cpuinfo_path=base["cpuinfo_path"],
|
||
ch_remote=base["ch_remote"], model=base["model"],
|
||
max_vms=quota["max_vms"], disk_mode=quota["disk_mode"],
|
||
disk_gb=quota["disk_gb"], ssh_host=ssh["ssh_host"],
|
||
ssh_forward=ssh["ssh_forward"],
|
||
reuse_experiment=quota["reuse_experiment"],
|
||
egress_proxy=quota["egress_proxy"],
|
||
allow_direct_egress=quota["allow_direct_egress"],
|
||
host_key_mode=ssh["host_key_mode"],
|
||
task_timeout=timeouts["task_timeout"],
|
||
snapshot_url=timeouts["snapshot_url"],
|
||
warm_pool_size=timeouts["warm_pool_size"],
|
||
api_timeout=timeouts["api_timeout"],
|
||
ready_timeout=timeouts["ready_timeout"],
|
||
ready_interval=timeouts["ready_interval"],
|
||
single_tenant_project=quota["single_tenant_project"],
|
||
snapshot_max_count=quota["snapshot_max_count"],
|
||
snapshot_max_total_gb=quota["snapshot_max_total_gb"],
|
||
snapshot_retention_days=quota["snapshot_retention_days"],
|
||
snapshot_dir=quota["snapshot_dir"],
|
||
allow_remote_snapshot=quota["allow_remote_snapshot"])
|
||
|
||
|
||
def build_chv_config(params: dict, env: dict):
|
||
"""把显式构造参数与环境变量解析成 ChvConfig(见
|
||
_assemble_chv_config 的组装段;片段化与校验顺序不变)。"""
|
||
env = env if env is not None else {}
|
||
base = {
|
||
"binary": params.get("binary") or env.get("CHV_BINARY")
|
||
or "cloud-hypervisor",
|
||
"kernel": params.get("kernel") or env.get("CHV_KERNEL"),
|
||
"rootfs": params.get("rootfs") or params.get("image")
|
||
or env.get("CHV_ROOTFS") or env.get("CHV_IMAGE"),
|
||
"cpus": str(params.get("cpus") or env.get("CHV_CPU") or "2"),
|
||
"memory": params.get("memory") or env.get("CHV_MEM") or "2G",
|
||
"state_dir": params.get("state_dir")
|
||
or env.get("CHV_STATE_DIR")
|
||
or os.path.join(os.path.expanduser("~"),
|
||
".local", "share", "codex-app-server", "chv-vms"),
|
||
"kvm_path": params.get("kvm_path")
|
||
if params.get("kvm_path") is not None else "/dev/kvm",
|
||
"cpuinfo_path": params.get("cpuinfo_path")
|
||
if params.get("cpuinfo_path") is not None else "/proc/cpuinfo",
|
||
"ch_remote": params.get("ch_remote") or env.get("CHV_REMOTE")
|
||
or "ch-remote",
|
||
"model": params.get("model"),
|
||
}
|
||
return _assemble_chv_config(
|
||
base, _ssh_fragment(params, env), _quota_fragment(env),
|
||
_timeout_fragment(params, env))
|