Files
codex-app-server/cloud/chv/config.py
T
host 9771ed9e8c feat: CHV 审核修复(P0+P1,堆叠于 #230)
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。
2026-10-31 01:54:13 +00:00

170 lines
8.0 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 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 build_chv_config(params, env):
"""把显式构造参数(params,None=未给)与环境变量(env,
默认为 os.environ 的调用方传入 dict)解析成 ChvConfig。
显式参数只有 __init__ 本来就有的那些;max_vms、磁盘模式、
磁盘配额、egress、快照、预热池目标数本来就没有显式参数,
仍只走环境变量(与 #216 一致,不悄悄加新参数面)。"""
env = env if env is not None else {}
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 "/tmp/chv-vms"
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 之间")
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")
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"
# P0-4 SSH 目标解析(feat-chv-fix):显式 CHV_SSH_HOST 优先;
# 其次取 VmConfig net 首个带 ip 的条目(静态 tap 客机 IP,
# 即选定的客机 IP 发现方案);两者皆无时只有显式
# CHV_SSH_FORWARD=1 才允许回环 127.0.0.1(运维宣称宿主
# NAT 已把该端口转发到客机 sshd),否则执行器在 wake/
# wait 时报 ssh_target_undeclared,绝不无条件假设回环能通。
# vsock 未选:需客内 agent + 宿主代理两套新代码(#216 已
# 否决);纯 NAT 方案未选:DNAT 是宿主防火墙外部状态,
# 执行器代码既不能创建也不能校验它,写进代码仍是假保证。
ssh_host = params.get("ssh_host") or env.get("CHV_SSH_HOST")
ssh_forward = env.get("CHV_SSH_FORWARD") == "1"
# P0-3:reuse 实验二次确认(名字即带实验含义)。
reuse_experiment = env.get(
"CHV_REUSE_SINGLE_TENANT_EXPERIMENT") == "1"
model = params.get("model")
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)
disk_gb_raw = env.get("CHV_DISK_GB")
disk_gb = float(disk_gb_raw) if disk_gb_raw else None
egress_proxy = env.get("CHV_EGRESS_PROXY")
allow_direct_egress = env.get("CHV_ALLOW_DIRECT_EGRESS") == "1"
host_key_mode = params.get("host_key_mode") \
or env.get("CHV_HOST_KEY_MODE") or "keyscan"
if host_key_mode not in ("provisioned", "keyscan"):
raise ValueError(
"CHV_HOST_KEY_MODE 只能是 provisioned 或 keyscan:%s"
% host_key_mode)
task_timeout_raw = env.get("CHV_TASK_TIMEOUT")
task_timeout = float(task_timeout_raw) \
if task_timeout_raw else None
snapshot_url = env.get("CHV_SNAPSHOT_URL")
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 ChvConfig(
binary=binary, kernel=kernel, rootfs=rootfs, cpus=cpus,
memory=memory, state_dir=state_dir, ssh_key=ssh_key,
ssh_user=ssh_user, ssh_port=ssh_port,
guest_host_py=guest_host_py, cmdline=cmdline, net=net,
net_json=net_json, kvm_path=kvm_path,
cpuinfo_path=cpuinfo_path, ch_remote=ch_remote, model=model,
max_vms=max_vms, disk_mode=disk_mode, disk_gb=disk_gb,
ssh_host=ssh_host, ssh_forward=ssh_forward,
reuse_experiment=reuse_experiment,
egress_proxy=egress_proxy,
allow_direct_egress=allow_direct_egress,
host_key_mode=host_key_mode,
task_timeout=task_timeout, snapshot_url=snapshot_url,
warm_pool_size=warm_pool_size, api_timeout=api_timeout,
ready_timeout=ready_timeout, ready_interval=ready_interval)