"""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" 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" 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, egress_proxy=egress_proxy, allow_direct_egress=allow_direct_egress, 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)