Files
codex-app-server/cloud/chv/config.py
T

236 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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))