feat(chv): 架构/可维护性/前端只读侧推进——生产实验分离、配置片段化、VM 只读展示
架构:WarmPool 从 cloud/chv/pool.py 移到 experimental_pool.py, 生产残留清理彻底分文件到 cloud/chv/orphans.py(facade 直接读 registry 文件清理,不构造池、不碰 prewarm/acquire/release); cloud/chv 与 cloud/sandbox 不再导出 WarmPool,旧路径一律 ImportError(CHV 未合并、无生产用户,兼容保留已无保留理由); worker 改走 orphans facade,不再持有 _warm_pool;#215 对齐从 接口形状推进到 REPLACEMENT_POINTS 替换清单(R1/R2 可机械替换、 R3 永不替换,#215 模块当前不存在由测试钉住)。 可维护性:build_chv_config 拆成 base/ssh/network/quota/disk/ timeout 六片段(组装只做 dict 合并,键冲突即报错;38 字段集 与取值不变,单项非法报错文案逐字一致);ADR-12 模块职责图 与禁 import 清单(AST 钉住);cloud/chv/ 公开 API 全量类型 标注、无 Any 逃逸,并修正三处旧标注错误;死代码扫一遍 (删实验池已无调用的 _quarantine 委托与未用 _LOGGER)。 前端(只读侧,不做控制按钮):chv.ts 新增纯逻辑谓词与文案 (执行器精确匹配、附件守卫、VM 状态文案、列表徽标);列表 行新增 VM 已报告死亡徽标;详情对全部 CHV 任务显示 VM 行 (已报告死亡/未报告死亡,不猜 live);WorkersSection 与成本 卡沿用已有数据,不新开池管理/成本页;服务端未新增第二个 生命周期字段(持久化事实只有三项,chv_vm_dead 已是完整派生), 只把派生抽成 _chv_vm_dead_from_row 并补 goal 回落形态测试。
This commit is contained in:
1 parent
42de20da68
commit
df8c64bd64
30 files changed
+1263
-331
No files matched your search
+23
-8
@@ -1,18 +1,33 @@
|
||||
"""Cloud Hypervisor 执行器包(从 cloud/sandbox.py 巨类拆出)。
|
||||
|
||||
公开名与 #216 一致,cloud/sandbox.py 会 re-export 它们,
|
||||
保证旧导入路径可用:
|
||||
公开名与 #216 一致,cloud/sandbox.py 会 re-export 生产
|
||||
接口,保证旧导入路径可用——但 WarmPool 除外(本轨破环
|
||||
性清理,见下):
|
||||
- CloudHypervisorExecutor:执行器本体(executor.py)
|
||||
- WarmPool:预热池 + registry 持久化与孤儿清理(pool.py)
|
||||
- ChvFailure:分类失败(errors.py)
|
||||
- reconcile_orphans_for_executor:生产残留清理唯一入口
|
||||
(orphans.py,只读 registry 文件做清理,不构造池)
|
||||
|
||||
子模块分工见 executor.py 头注释。本包顶层 import 不触
|
||||
cloud.sandbox(执行器方法内按需 import 共享 helper),
|
||||
所以任何导入顺序都不会循环。
|
||||
破环性清理(feat-chv-arch-maint-fe,未合并前即生效,
|
||||
合并时按 PR 描述的清单检查调用方):WarmPool 已从
|
||||
cloud/chv/pool.py 移到 cloud/chv/experimental_pool.py,
|
||||
本包与 cloud/sandbox.py 不再导出它。旧路径
|
||||
`from cloud.chv import WarmPool`、
|
||||
`from cloud.chv.pool import WarmPool`、
|
||||
`from cloud.sandbox import WarmPool` 一律 ImportError;
|
||||
实验用法只走
|
||||
`from cloud.chv.experimental_pool import WarmPool`。
|
||||
清理理由:CHV 尚未合并、尚无生产用户,兼容保留只是
|
||||
“怕断 import”,留着反而让生产代码有路可走进实验接口。
|
||||
|
||||
子模块分工见 executor.py 头注释与 docs/chv-architecture.md
|
||||
ADR-12。本包顶层 import 不触 cloud.sandbox(执行器方法
|
||||
内按需 import 共享 helper),所以任何导入顺序都不会循环。
|
||||
"""
|
||||
|
||||
from cloud.chv.errors import ChvFailure
|
||||
from cloud.chv.executor import CloudHypervisorExecutor
|
||||
from cloud.chv.pool import WarmPool
|
||||
from cloud.chv.orphans import reconcile_orphans_for_executor
|
||||
|
||||
__all__ = ["ChvFailure", "CloudHypervisorExecutor", "WarmPool"]
|
||||
__all__ = ["ChvFailure", "CloudHypervisorExecutor",
|
||||
"reconcile_orphans_for_executor"]
|
||||
@@ -128,7 +128,7 @@ def _unix_socket_api(socket_path, method, path, payload=None):
|
||||
return _parse_response(header, body)
|
||||
|
||||
|
||||
def normalize_api_result(result) -> tuple:
|
||||
def normalize_api_result(result: dict) -> tuple:
|
||||
"""API 调用归一化:fake api_client 可返回 (status, body)
|
||||
或 {"status":..., "body":...} 或带 .status 的对象,本
|
||||
函数统一成 (status:int, body)。"""
|
||||
|
||||
+57
-2
@@ -23,12 +23,67 @@ cloud/gates.py、cloud/quota.py、cloud/sandbox_policy.py。
|
||||
不提供,测试钉住“CHV 不 import 它们”。
|
||||
|
||||
本模块只依赖标准库,是叶子模块,不 import cloud.sandbox。
|
||||
|
||||
合并后替换清单(REPLACEMENT_POINTS,本轨从“接口形状”
|
||||
推进到可机械执行的清单;#215 未合并前一律不 import
|
||||
cloud.gates / cloud.sandbox_policy / cloud.timeouts /
|
||||
cloud.quota,测试钉住这些模块在本基线里不存在):
|
||||
- R1 gates.resolve_inside:唯一生产调用方是
|
||||
hardening._is_within(快照 URL 白名单)。合并后两步:
|
||||
① hardening 改为 `from cloud.gates import resolve_inside`;
|
||||
② 删本模块 resolve_inside。语义必须逐字一致,改完
|
||||
重跑快照白名单测试(逃逸/符号链接/绝对路径三例)。
|
||||
- R2 sandbox_policy.tmpfs_isolated:形状占位,当前无
|
||||
生产调用方(只有测试)。合并后一步:删本模块
|
||||
tmpfs_isolated;将来有调用方时直接 import #215 版,
|
||||
不得再本地复刻。CHV 语义是“一律不隔离(fail-closed)”,
|
||||
若 #215 版对 cloud-hypervisor 返回 True,替换即改行为,
|
||||
必须先停下来核对,不得机械替换。
|
||||
- R3 timeouts / quota:永不替换。#215 的 timeouts 是
|
||||
审批/队列超时(None/0 = 不过期),quota 是按月美元
|
||||
配额;CHV 的 api/ready 超时是秒数(0 无效)、配额是
|
||||
VM 数/磁盘 GB/快照数,单位与语义都不同。清单里明写
|
||||
“不替换”,防止合并时有人按名字硬套。
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def resolve_inside(base, rel):
|
||||
# 合并后替换清单:每项的 local/replacement/test 字段由测试
|
||||
# 逐项核对(local 函数真实存在、#215 模块当前不存在、
|
||||
# test 命名的测试类真实存在),清单本身不得漂移。
|
||||
REPLACEMENT_POINTS = (
|
||||
{
|
||||
"id": "R1",
|
||||
"upstream": "cloud.gates.resolve_inside",
|
||||
"local": "cloud.chv.compat215.resolve_inside",
|
||||
"caller": "cloud.chv.hardening._is_within",
|
||||
"replacement": "hardening 改为 from cloud.gates import "
|
||||
"resolve_inside,并删除本地 resolve_inside",
|
||||
"test": "Compat215Test + 快照白名单测试",
|
||||
},
|
||||
{
|
||||
"id": "R2",
|
||||
"upstream": "cloud.sandbox_policy.tmpfs_isolated",
|
||||
"local": "cloud.chv.compat215.tmpfs_isolated",
|
||||
"caller": None,
|
||||
"replacement": "删除本地 tmpfs_isolated;有调用方时"
|
||||
"直接 import #215 版(先核对 CHV 一律 False 语义)",
|
||||
"test": "Compat215Test.test_tmpfs_isolated_chv_and_unknown_"
|
||||
"fail_closed",
|
||||
},
|
||||
{
|
||||
"id": "R3",
|
||||
"upstream": "cloud.timeouts / cloud.quota",
|
||||
"local": None,
|
||||
"caller": None,
|
||||
"replacement": None,
|
||||
"test": "Compat215Test.test_no_timeout_or_quota_map",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def resolve_inside(base: str, rel: str) -> str | None:
|
||||
"""目录门:base 内的相对路径安全解析,越界回 None。
|
||||
|
||||
空串回 base 本身;绝对路径、.. 逃逸、符号链接逃逸、
|
||||
@@ -48,7 +103,7 @@ def resolve_inside(base, rel):
|
||||
return target
|
||||
|
||||
|
||||
def tmpfs_isolated(executor_kind, path):
|
||||
def tmpfs_isolated(executor_kind: str, path: str) -> tuple[bool, str]:
|
||||
"""某执行器下某路径是否走 tmpfs 隔离:(isolated, reason)。
|
||||
|
||||
cloud-hypervisor:客机 /tmp 落在客机磁盘上,不走宿主
|
||||
|
||||
+102
-84
@@ -15,20 +15,19 @@ cloud.sandbox,避免循环导入)。
|
||||
直接 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 描述的复用清单。
|
||||
本轨不假装复用它。上一轨 ADR-7 的泛化映射已被证伪
|
||||
(#215 的 timeouts 是审批/队列超时、quota 是按月美元
|
||||
配额,与 CHV 的秒数超时、VM 数/磁盘 GB 单位语义不同,
|
||||
硬套会改行为),真实映射只有两处,见
|
||||
cloud/chv/compat215.py 的 REPLACEMENT_POINTS 清单
|
||||
(合并时按清单机械替换,不在本轨硬 import 未合并模块)。
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
def _parse_memory_bytes(value):
|
||||
def _parse_memory_bytes(value: str) -> int:
|
||||
"""内存字符串转字节:2G/512M/1024K/2048(无单位=字节)。
|
||||
非法值 ValueError,绝不静默按默认算。"""
|
||||
m = re.match(r"^\s*(\d+)\s*([KMGkmg]?)\s*[Bb]?\s*$",
|
||||
@@ -45,15 +44,44 @@ class ChvConfig:
|
||||
执行器 __init__ 直接把字段抄到 self 上,旧测试读属性
|
||||
的方式全部不变。"""
|
||||
|
||||
def __init__(self, **fields):
|
||||
def __init__(self, **fields: object) -> None:
|
||||
for key, value in fields.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
|
||||
def _base_fragment(params: dict, env: dict) -> dict:
|
||||
"""身份/资源基底片段:二进制、内核、根盘、CPU/内存、
|
||||
state_dir、KVM/cpuinfo 路径、ch-remote、模型。无校验,
|
||||
取值口径与旧 build_chv_config 内联 base 逐字一致。"""
|
||||
return {
|
||||
"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"),
|
||||
}
|
||||
|
||||
|
||||
def _ssh_fragment(params: dict, env: dict) -> dict:
|
||||
"""SSH/命令面配置片段:返回 dict,供 build_chv_config 组装。
|
||||
错误文案与原内联代码逐字一致,校验时机不变(构造时)。"""
|
||||
"""SSH/命令面片段:密钥、用户、端口、host key 模式、
|
||||
客内 host.py 路径、内核命令行。错误文案与原内联代码
|
||||
逐字一致,校验时机不变(构造时)。网络字段(net、
|
||||
ssh_host、egress)已移到 _network_fragment,不在本
|
||||
片段,避免“SSH”片段名不副实。"""
|
||||
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") == "":
|
||||
@@ -82,19 +110,45 @@ def _ssh_fragment(params: dict, env: dict) -> dict:
|
||||
"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",
|
||||
}
|
||||
|
||||
|
||||
def _network_fragment(params: dict, env: dict) -> dict:
|
||||
"""网络片段:tap/桥接描述、SSH 目标、端口转发开关、
|
||||
egress 代理与直连开关。无数值校验(校验在 preflight
|
||||
的 net/egress 门里,不在配置解析层)。egress 从旧
|
||||
quota 片段移到这里:它是网络出口策略,不是 VM 配额。"""
|
||||
return {
|
||||
"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",
|
||||
"egress_proxy": env.get("CHV_EGRESS_PROXY"),
|
||||
"allow_direct_egress": env.get("CHV_ALLOW_DIRECT_EGRESS")
|
||||
== "1",
|
||||
}
|
||||
|
||||
|
||||
def _quota_fragment(env: dict) -> dict:
|
||||
"""配额/磁盘/快照配置片段:返回 dict,错误文案逐字一致。"""
|
||||
"""配额片段:仅容量数——最大并发 VM 数与预热池大小。
|
||||
磁盘/快照数上限不在这里,它们是磁盘行为,见
|
||||
_disk_fragment。校验文案与旧代码逐字一致。"""
|
||||
try:
|
||||
max_vms = int(env.get("CHV_MAX_VMS") or 4)
|
||||
except ValueError:
|
||||
raise ValueError("CHV_MAX_VMS 必须是整数")
|
||||
try:
|
||||
warm_pool_size = int(env.get("CHV_WARM_POOL_SIZE") or 0)
|
||||
except ValueError:
|
||||
raise ValueError("CHV_WARM_POOL_SIZE 必须是整数")
|
||||
return {"max_vms": max_vms, "warm_pool_size": warm_pool_size}
|
||||
|
||||
|
||||
def _disk_fragment(env: dict) -> dict:
|
||||
"""磁盘片段:磁盘模式/大小、单租户 reuse 实验开关、
|
||||
快照数/总量/保留期/目录/远端开关与快照 URL。校验
|
||||
文案与旧 quota 片段逐字一致(disk_mode 白名单、
|
||||
disk_gb > 0、快照数非负)。"""
|
||||
disk_mode = env.get("CHV_DISK_MODE") or "copy"
|
||||
if disk_mode not in ("copy", "reuse"):
|
||||
raise ValueError(
|
||||
@@ -119,8 +173,7 @@ def _quota_fragment(env: dict) -> dict:
|
||||
or snapshot_retention_days < 0:
|
||||
raise ValueError("CHV_SNAPSHOT_* 不能为负数")
|
||||
return {
|
||||
"max_vms": max_vms, "disk_mode": disk_mode,
|
||||
"disk_gb": disk_gb,
|
||||
"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"),
|
||||
@@ -130,21 +183,17 @@ def _quota_fragment(env: dict) -> dict:
|
||||
"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",
|
||||
"snapshot_url": env.get("CHV_SNAPSHOT_URL"),
|
||||
}
|
||||
|
||||
|
||||
def _timeout_fragment(params: dict, env: dict) -> dict:
|
||||
"""超时/池/快照 URL 配置片段:返回 dict,错误文案逐字一致。"""
|
||||
"""超时片段:任务超时(None = 不限,旧语义)、VMM API
|
||||
超时、客机就绪超时/间隔。ready_interval <= 0 时回落
|
||||
5 秒(旧语义,非校验失败)。校验文案逐字一致。"""
|
||||
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")
|
||||
@@ -163,73 +212,42 @@ def _timeout_fragment(params: dict, env: dict) -> dict:
|
||||
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,
|
||||
"task_timeout": task_timeout, "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。
|
||||
def _assemble_chv_config(fragments: list) -> "ChvConfig":
|
||||
"""把各片段 dict 合并成 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"])
|
||||
片段键集必须互不相交——键冲突是编程错误,直接
|
||||
ValueError,绝不让后一片段静默覆盖前一片段的值。"""
|
||||
fields: dict = {}
|
||||
for fragment in fragments:
|
||||
for key, value in fragment.items():
|
||||
if key in fields:
|
||||
raise ValueError(
|
||||
"CHV 配置片段键冲突:%s" % key)
|
||||
fields[key] = value
|
||||
return ChvConfig(**fields)
|
||||
|
||||
|
||||
def build_chv_config(params: dict, env: dict):
|
||||
"""把显式构造参数与环境变量解析成 ChvConfig(见
|
||||
_assemble_chv_config 的组装段;片段化与校验顺序不变)。"""
|
||||
def build_chv_config(params: dict, env: dict) -> "ChvConfig":
|
||||
"""把显式构造参数与环境变量解析成 ChvConfig。
|
||||
|
||||
片段调用顺序即旧校验顺序(SSH → 配额 → 磁盘 →
|
||||
超时),只有一处可观测的顺序变化:warm_pool_size
|
||||
从旧超时片段移到配额片段,因此“CHV_WARM_POOL_SIZE
|
||||
非法 + 磁盘非法同时出现”时先报前者;单项非法时
|
||||
报错与旧代码逐字一致。字段集合与取值与旧代码一致,
|
||||
由测试的字段集断言钉住。"""
|
||||
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))
|
||||
return _assemble_chv_config([
|
||||
_base_fragment(params, env),
|
||||
_ssh_fragment(params, env),
|
||||
_quota_fragment(env),
|
||||
_disk_fragment(env),
|
||||
_network_fragment(params, env),
|
||||
_timeout_fragment(params, env),
|
||||
])
|
||||
+8
-5
@@ -33,7 +33,7 @@ def _dir_total_bytes(path):
|
||||
return total
|
||||
|
||||
|
||||
def check_disk_quota(rootfs: str, disk_gb: float, state_dir=None) -> None:
|
||||
def check_disk_quota(rootfs: str, disk_gb: float, state_dir: str | None = None) -> None:
|
||||
"""磁盘配额门(feat-chv-fix 口径):
|
||||
|
||||
- disk_gb 未设时不检查;
|
||||
@@ -162,8 +162,10 @@ def _save_snapshots(state_dir, vm_name, entries):
|
||||
os.replace(tmp_path, path)
|
||||
|
||||
|
||||
def prune_snapshots(state_dir, vm_name, retention_days,
|
||||
now=None, delete_files=True):
|
||||
def prune_snapshots(state_dir: str, vm_name: str,
|
||||
retention_days: float | None,
|
||||
now: float | None = None,
|
||||
delete_files: bool = True) -> tuple[list, list]:
|
||||
"""按保留期剪枝:删过期条目;delete_files 时同时删
|
||||
对应的 file:// 快照文件(只删登记里记的 path,不猜
|
||||
文件名)。返回(保留条目,剪掉条目)。"""
|
||||
@@ -189,7 +191,8 @@ def prune_snapshots(state_dir, vm_name, retention_days,
|
||||
|
||||
|
||||
def check_snapshot_limits(state_dir: str, vm_name: str, max_count: int,
|
||||
max_total_gb, retention_days):
|
||||
max_total_gb: float,
|
||||
retention_days: float | None) -> list:
|
||||
"""快照上限预检(snapshot_vm 调 ch-remote 之前调):
|
||||
先按保留期剪枝,再查数量(已有数 + 本次 > 上限即
|
||||
snapshot_quota_exceeded)与总量(已有总量已达上限即
|
||||
@@ -218,7 +221,7 @@ def check_snapshot_limits(state_dir: str, vm_name: str, max_count: int,
|
||||
return kept
|
||||
|
||||
|
||||
def record_snapshot(state_dir: str, vm_name: str, snapshot_url: str, max_total_gb: float) -> dict:
|
||||
def record_snapshot(state_dir: str, vm_name: str, snapshot_url: str, max_total_gb: float) -> tuple[list, list]:
|
||||
"""快照成功后登记;file:// 目标记实际大小。登记后若
|
||||
总量超上限,按最旧优先删登记外的旧快照文件(总量硬
|
||||
帽,删动作由调用方审计——本函数返回被删的旧条目)。
|
||||
|
||||
+1
-1
@@ -7,6 +7,6 @@ class ChvFailure(RuntimeError):
|
||||
审计与失败分类用,绝不笼统只写 failed。kind 取值见
|
||||
CloudHypervisorExecutor.preflight / wake_vm 的注释。"""
|
||||
|
||||
def __init__(self, kind, message):
|
||||
def __init__(self, kind: str, message: str) -> None:
|
||||
super().__init__("chv_failure:%s: %s" % (kind, message))
|
||||
self.kind = kind
|
||||
+66
-50
@@ -7,7 +7,8 @@
|
||||
- ssh_transport.py:SSH 命令构造、每 VM known_hosts 钉死、
|
||||
ssh-keyscan 首次捕获(TOFU 边界见该模块)
|
||||
- disk.py:每任务磁盘供给与磁盘配额门
|
||||
- pool.py:WarmPool 预热池 + registry 持久化与孤儿清理
|
||||
- orphans.py:生产残留清理唯一入口(registry 文件直读清理)
|
||||
- experimental_pool.py:WarmPool 预热池(纯实验,生产禁 import)
|
||||
- executor.py(本模块):执行器类本体——生命周期方法
|
||||
(wake/pause/resume/snapshot/restore/hibernate/stop/
|
||||
cleanup)与任务执行方法(dispatch/run/goal/execute)。
|
||||
@@ -154,11 +155,11 @@ class CloudHypervisorExecutor(Executor):
|
||||
- 单任务单 VM:wake → 跑 → finally 关,同一任务不共享 VM;
|
||||
每任务磁盘 copy 模式独立,租户无共享路径、无东西向互访;
|
||||
VM 内 root 只能影响本 VM。
|
||||
- 快速启动:WarmPool(cloud/chv/pool.py)预热干净快照 VM,
|
||||
- 快速启动:WarmPool(cloud/chv/experimental_pool.py)预热干净快照 VM,
|
||||
acquire 消耗、release 销毁,池空回落冷启动,绝不降级;
|
||||
池状态另落 state_dir/warm-pool.json registry,worker
|
||||
重启后载入的条目一律当孤儿、只能 reconcile_orphans()
|
||||
清理,绝不重新发给任务(见 pool.py)。
|
||||
清理,绝不重新发给任务(见 orphans.py)。
|
||||
- 长任务中途死亡:_run_with_vm 在客内命令抛传输异常、
|
||||
或非 goal 结果已 failed 且 VM 登记进程已死时,改判
|
||||
interrupted(断点可续跑)而不是笼统 failed;VM 还活
|
||||
@@ -245,17 +246,30 @@ class CloudHypervisorExecutor(Executor):
|
||||
开启嵌套虚拟化的实例,那是用户侧开通决策,不在本轨内。
|
||||
"""
|
||||
|
||||
def __init__(self, binary=None, kernel=None, rootfs=None,
|
||||
image=None, cpus=None, memory=None, state_dir=None,
|
||||
runner=None, launcher=None, ssh_key=None,
|
||||
ssh_user=None, ssh_port=None,
|
||||
guest_host_py="/app/host.py", cmdline=None,
|
||||
net=None, net_json=None, ssh_host=None,
|
||||
kvm_path="/dev/kvm",
|
||||
cpuinfo_path="/proc/cpuinfo", env=None,
|
||||
model=None, ch_remote=None, api_client=None,
|
||||
sleeper=None, api_timeout=None, ready_timeout=None,
|
||||
ready_interval=None, host_key_mode=None):
|
||||
def __init__(self, binary: str | None = None,
|
||||
kernel: str | None = None,
|
||||
rootfs: str | None = None,
|
||||
image: str | None = None,
|
||||
cpus: str | int | None = None,
|
||||
memory: str | None = None,
|
||||
state_dir: str | None = None,
|
||||
runner: object = None, launcher: object = None,
|
||||
ssh_key: str | None = None,
|
||||
ssh_user: str | None = None,
|
||||
ssh_port: int | str | None = None,
|
||||
guest_host_py: str = "/app/host.py",
|
||||
cmdline: str | None = None,
|
||||
net: str | None = None, net_json: str | None = None,
|
||||
ssh_host: str | None = None,
|
||||
kvm_path: str = "/dev/kvm",
|
||||
cpuinfo_path: str = "/proc/cpuinfo",
|
||||
env: dict | None = None,
|
||||
model: str | None = None, ch_remote: str | None = None,
|
||||
api_client: object = None,
|
||||
sleeper: object = None, api_timeout: float | None = None,
|
||||
ready_timeout: float | None = None,
|
||||
ready_interval: float | None = None,
|
||||
host_key_mode: str | None = None) -> None:
|
||||
self._env = env if env is not None else os.environ
|
||||
# 配置解析已拆到 cloud/chv/config.py(参数优先于环境
|
||||
# 变量,默认值与 #216 一致;显式空用户/越界端口的收紧
|
||||
@@ -326,7 +340,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
return re.sub(r"[^A-Za-z0-9_.-]", "-",
|
||||
str(task.get("id")))
|
||||
|
||||
def vm_name(self, task_or_name):
|
||||
def vm_name(self, task_or_name: dict | str) -> str:
|
||||
"""VM 名:chv-<安全字符任务名>(与 Docker 的 codex- 同款
|
||||
派生,同一任务每次算出同一名字,清理才能按名找)。
|
||||
幂等:传入的已是 chv- 前缀名时不再二次加前缀(清理
|
||||
@@ -339,7 +353,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
return safe
|
||||
return "chv-" + safe
|
||||
|
||||
def api_socket(self, task_or_name):
|
||||
def api_socket(self, task_or_name: dict | str) -> str:
|
||||
"""该 VM 的 api-socket 路径:state_dir/<vm 名>.sock。"""
|
||||
return os.path.join(self.state_dir,
|
||||
self.vm_name(task_or_name) + ".sock")
|
||||
@@ -559,7 +573,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
validate_net_entries(_entries, [])
|
||||
except ChvFailure as exc:
|
||||
missing.append((exc.kind, str(exc)))
|
||||
def preflight(self):
|
||||
def preflight(self) -> bool:
|
||||
"""预检(缺一不可则 fail-fast):收集全部缺失项一次报出
|
||||
具体缺哪一项,绝不笼统报错;失败为 ChvFailure(带 kind,
|
||||
供审计与失败分类用),绝不回落 Docker/Local。
|
||||
@@ -651,7 +665,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
cmd += ["--net", self.net]
|
||||
return cmd
|
||||
|
||||
def known_hosts_path(self, task_or_name):
|
||||
def known_hosts_path(self, task_or_name: dict | str) -> str:
|
||||
"""该 VM 的 known_hosts 文件:state_dir/<vm>/known_hosts
|
||||
(与每任务磁盘同目录,见 ssh_transport.py 的钉死模型)。"""
|
||||
return _known_hosts_path(self.state_dir,
|
||||
@@ -671,7 +685,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
return str(entry["ip"])
|
||||
return None
|
||||
|
||||
def resolve_ssh_host(self):
|
||||
def resolve_ssh_host(self) -> str:
|
||||
"""SSH 目标解析(feat-chv-fix,P0-4 选定方案:客机 IP):
|
||||
|
||||
显式 CHV_SSH_HOST > net 静态客机 IP > 显式转发宣称下
|
||||
@@ -690,7 +704,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
"里给 net 条目配 ip(静态 tap 客机 IP)、或显式设 "
|
||||
"CHV_SSH_FORWARD=1 宣称宿主 NAT 已转发回环端口")
|
||||
|
||||
def provision_host_key(self, task, secret_env=None):
|
||||
def provision_host_key(self, task: dict, secret_env: dict | None = None) -> dict:
|
||||
"""按 VM 预置 host key(补缺口轨强方案,见 hostkeys.py):
|
||||
启动前为该 VM 生成一次性 ed25519 host key 对,公钥
|
||||
立即写进该 VM 的 known_hosts(钉死),私钥只写进
|
||||
@@ -741,7 +755,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
return {"paths": paths, "seed_dir": seed_dir,
|
||||
"known_hosts": paths["known_hosts"]}
|
||||
|
||||
def capture_host_key(self, task):
|
||||
def capture_host_key(self, task: dict) -> bool:
|
||||
"""首次捕获客机 host key(TOFU,弱回落,边界见
|
||||
ssh_transport.py 与 hostkeys.py):ssh-keyscan 经本机
|
||||
回环端口抓 key 行,写入该 VM 的 known_hosts 文件
|
||||
@@ -775,7 +789,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
"vm=%s host=%s" % (self.vm_name(task), ssh_host))
|
||||
return True
|
||||
|
||||
def build_ssh_command(self, task, inner_cmd, extra_env=None):
|
||||
def build_ssh_command(self, task: dict, inner_cmd: list, extra_env: dict | None = None) -> list:
|
||||
"""构造 SSH 命令:ssh -p <端口> -i <key 文件> + 钉死选项
|
||||
+ user@<SSH 目标> + 客内命令。argv 里只有 key 文件路径,
|
||||
绝无任何 key 值。egress_proxy 已设时客内命令前加 env
|
||||
@@ -821,7 +835,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
guest_env_file=guest_env_file)
|
||||
|
||||
# -- API 模式:VmConfig / API 调用 / 就绪轮询 -----------------------
|
||||
def build_vm_config(self, task_disk=None, seed_disk=None):
|
||||
def build_vm_config(self, task_disk: str | None = None, seed_disk: str | None = None) -> dict:
|
||||
"""API 模式的 VmConfig(vm.create 请求体,纯函数):
|
||||
cpus{boot_vcpus,max_vcpus}、memory{size: 字节}、
|
||||
kernel{path}、cmdline{args}、disks[{path: 每任务磁盘}]、
|
||||
@@ -968,7 +982,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
self.state_dir, self.vm_name(task), self.rootfs,
|
||||
self.disk_mode)
|
||||
|
||||
def wait_api_ready(self, task):
|
||||
def wait_api_ready(self, task: dict) -> bool:
|
||||
"""等 vmm.ping 通:按次数轮询(api_timeout/1 秒取整,
|
||||
至少 1 次),不用墙钟死循环——fake sleeper 不推进墙钟
|
||||
时墙钟循环会空转,次数循环在单测里是确定性的。
|
||||
@@ -1014,7 +1028,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
return True
|
||||
return False
|
||||
|
||||
def wait_guest_ready(self, task):
|
||||
def wait_guest_ready(self, task: dict) -> bool:
|
||||
"""等 guest SSH 就绪:轮询 ssh "true",returncode 0 即就绪;
|
||||
每轮先尽力捕获 host key(keyscan 模式),超时先关 VM
|
||||
再 raise guest_not_ready,绝不在未就绪时派任务。"""
|
||||
@@ -1298,7 +1312,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
return task_disk, socket_path
|
||||
|
||||
|
||||
def wake_vm(self, task, extra_env=None):
|
||||
def wake_vm(self, task: dict, extra_env: dict | None = None) -> dict:
|
||||
"""主唤醒路径(API 模式,dispatch/run/goal 的前置步骤):
|
||||
preflight → 容量门 → 磁盘配额门 → 每任务磁盘供给 →
|
||||
launcher 只起 VMM(--api-socket)→ 等 vmm.ping 通 →
|
||||
@@ -1335,14 +1349,14 @@ class CloudHypervisorExecutor(Executor):
|
||||
return {"vm": name, "api_socket": socket_path,
|
||||
"state": "running", "ready": True, "mode": "cold"}
|
||||
|
||||
def start_vm(self, task):
|
||||
def start_vm(self, task: dict) -> dict:
|
||||
"""旧名保留:即 wake_vm(API 模式)。直接启动命令
|
||||
_build_vm_command() 只作手动等价写法(私有),不再是执行器
|
||||
的启动路径——两条启动路径并存是上一版的半吊子,
|
||||
本版只认 API 模式。"""
|
||||
return self.wake_vm(task)
|
||||
|
||||
def pause_vm(self, task_or_name):
|
||||
def pause_vm(self, task_or_name: dict | str) -> bool:
|
||||
"""暂停 VM:ch-remote pause。成功审计 vm_pause。"""
|
||||
self._ch_remote(task_or_name, "pause")
|
||||
task = task_or_name if isinstance(task_or_name, dict) \
|
||||
@@ -1351,7 +1365,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
"vm=%s" % self.vm_name(task_or_name))
|
||||
return True
|
||||
|
||||
def resume_vm(self, task_or_name):
|
||||
def resume_vm(self, task_or_name: dict | str) -> bool:
|
||||
"""唤醒已暂停的 VM:ch-remote resume。成功审计 vm_resume。
|
||||
注意:这是暂停唤醒的唯一路径,恢复后的 VM 也走它
|
||||
(见 restore_vm),绝不能只 restore 就算醒。"""
|
||||
@@ -1362,7 +1376,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
"vm=%s" % self.vm_name(task_or_name))
|
||||
return True
|
||||
|
||||
def snapshot_vm(self, task_or_name, snapshot_url):
|
||||
def snapshot_vm(self, task_or_name: dict | str, snapshot_url: str) -> str:
|
||||
"""给(已暂停的)VM 做快照:ch-remote snapshot
|
||||
<source_url>。snapshot_url 为空即 ValueError;成功
|
||||
审计 vm_snapshot。快照本身不改变 VM 状态(仍 paused,
|
||||
@@ -1454,7 +1468,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
self.resume_vm(task)
|
||||
self.wait_guest_ready(task)
|
||||
|
||||
def restore_vm(self, task, snapshot_url):
|
||||
def restore_vm(self, task: dict, snapshot_url: str) -> dict:
|
||||
"""快照恢复唤醒:校验 → 只起 VMM → restore → 显式
|
||||
resume → 等 SSH 就绪。恢复后默认 paused,不 resume
|
||||
就不算醒。任一步失败都关 VM 再抛;成功返回
|
||||
@@ -1474,7 +1488,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
return {"vm": name, "api_socket": self.api_socket(task),
|
||||
"state": "running", "ready": True, "mode": "restored"}
|
||||
|
||||
def hibernate_vm(self, task_or_name, snapshot_url):
|
||||
def hibernate_vm(self, task_or_name: dict | str, snapshot_url: str) -> dict:
|
||||
"""休眠(CHV 无原生 hibernate 端点,等价实现):
|
||||
pause + snapshot + shutdown。休眠后文件与 worktree
|
||||
保留在快照里(快照是暂停时刻的完整状态),临时文件
|
||||
@@ -1489,7 +1503,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
return {"vm": self.vm_name(task_or_name),
|
||||
"state": "hibernated", "snapshot": snapshot_url}
|
||||
|
||||
def wake_from_hibernate(self, task, snapshot_url):
|
||||
def wake_from_hibernate(self, task: dict, snapshot_url: str) -> dict:
|
||||
"""从休眠唤醒:即 restore_vm(restore + 显式 resume
|
||||
+ SSH 就绪),语义与 restore_vm 完全一致,只是名字
|
||||
说明来源是休眠快照。"""
|
||||
@@ -1560,7 +1574,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def stop_vm(self, task_or_name):
|
||||
def stop_vm(self, task_or_name: dict | str) -> bool:
|
||||
"""停止 VM(尽力而为,不抛异常):已死进程直接清登记,
|
||||
已关 VM 仍尽力调一次 ch-remote(返回 False),socket
|
||||
残留删 socket(返回 True)。ch-remote shutdown 失败
|
||||
@@ -1597,7 +1611,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
_LOGGER.info("chv_vm_stop vm=%s had=%s", name, had)
|
||||
return had
|
||||
|
||||
def gc_stale_disks(self):
|
||||
def gc_stale_disks(self) -> dict:
|
||||
"""残留盘 GC(H3):扫描 state_dir/*/disk.raw,VM
|
||||
非 live(无 api-socket 文件且无登记活进程)即删盘
|
||||
并审计;删失败审计 vm_disk_delete_failed 并计入
|
||||
@@ -1675,7 +1689,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
pass
|
||||
return removed_priv, removed_seed
|
||||
|
||||
def cleanup_stale_secrets(self):
|
||||
def cleanup_stale_secrets(self) -> list:
|
||||
"""崩溃残留扫描(H5):私钥一律删、非 live VM 的 seed
|
||||
同理删并审计;返回已清理 VM 名。worker 启动与
|
||||
cleanup_stale_vms 时调它,不依赖 socket 是否存在。"""
|
||||
@@ -1700,18 +1714,18 @@ class CloudHypervisorExecutor(Executor):
|
||||
% (entry, removed_priv, removed_seed))
|
||||
return cleaned
|
||||
|
||||
def shutdown_vm(self, task_or_name):
|
||||
def shutdown_vm(self, task_or_name: dict | str) -> bool:
|
||||
"""关闭入口的正式名:即 stop_vm(ch-remote shutdown)。
|
||||
任务结束/失败/取消时调它。"""
|
||||
return self.stop_vm(task_or_name)
|
||||
|
||||
def cancel_vm(self, task_or_name):
|
||||
def cancel_vm(self, task_or_name: dict | str) -> bool:
|
||||
"""取消入口:即 shutdown_vm。worker 的取消路径对
|
||||
Docker 调 kill_container,对 CHV 调本方法——取消绝不
|
||||
只改数据库状态,必须真关 VM。"""
|
||||
return self.shutdown_vm(task_or_name)
|
||||
|
||||
def recycle_vm(self, task):
|
||||
def recycle_vm(self, task: dict) -> dict:
|
||||
"""回收 VM(任务结束后的销毁语义):
|
||||
先 shutdown,再返回保留/丢失清单。保留的只有宿主侧
|
||||
的 task_checkpoints / task_attempts(数据库里的断点与
|
||||
@@ -1767,7 +1781,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
if dead:
|
||||
self._vms.pop(name, None)
|
||||
|
||||
def cleanup_stale_vms(self):
|
||||
def cleanup_stale_vms(self) -> list:
|
||||
"""清理残留 VM:先跑残留盘 GC 与残留密钥扫描,再走
|
||||
socket 扫描与登记清理。只清 socket 残留与已死进程
|
||||
对应的 VM;state_dir 不存在返回 [],绝不抛异常。"""
|
||||
@@ -1907,7 +1921,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
"""每任务 VM 生命周期(冷启动正式路径,feat-chv-arch95
|
||||
定案):wake_vm(API 模式 + SSH 就绪)→ 跑客内命令 →
|
||||
finally shutdown_vm。本方法绝不查 WarmPool:预热池是
|
||||
实验性的(见 pool.py 头定案与 ADR-5),dispatch 永远
|
||||
实验性的(见 experimental_pool.py 头定案与 ADR-5),dispatch 永远
|
||||
冷启动,warm_pool_wired_to_dispatch 恒 False。
|
||||
wake 失败时不跑
|
||||
任务、直接抛分类失败。本方法不做内部重试:重试是
|
||||
@@ -1951,7 +1965,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def execute(self, task) -> dict:
|
||||
def execute(self, task: dict) -> dict:
|
||||
"""只读执行:客内 host.py status,不直接跑模型回合
|
||||
(与 Local/Docker 的 execute 同款语义)。
|
||||
|
||||
@@ -1987,7 +2001,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
"SSH 失败归类为未运行:%s" % exc)
|
||||
raise
|
||||
|
||||
def dispatch_task(self, task, extra_env=None, model_provider=None) -> dict:
|
||||
def dispatch_task(self, task: dict, extra_env: dict | None = None, model_provider: str | None = None) -> dict:
|
||||
"""客内 host.py dispatch:建 host 任务并跑一轮 turn。"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
@@ -2004,7 +2018,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
model_provider=model_provider,
|
||||
sandbox=("read-only" if task.get("plan") else None))
|
||||
|
||||
def run_task(self, task, extra_env=None, model_provider=None) -> dict:
|
||||
def run_task(self, task: dict, extra_env: dict | None = None, model_provider: str | None = None) -> dict:
|
||||
"""客内 host.py run:在已有 host 任务上再跑一轮 turn。"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
@@ -2017,7 +2031,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
effort=task.get("effort"), approval=task.get("approval"),
|
||||
model_provider=model_provider)
|
||||
|
||||
def goal_task(self, task, extra_env=None, model_provider=None) -> dict:
|
||||
def goal_task(self, task: dict, extra_env: dict | None = None, model_provider: str | None = None) -> dict:
|
||||
"""客内 host.py goal:目标循环,轮次规则与 Local 同款
|
||||
(缺省 3 轮,>100 轮 loud fail,旧验收形态 0 轮禁用)。"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
@@ -2048,8 +2062,10 @@ class CloudHypervisorExecutor(Executor):
|
||||
effort=task.get("effort"), approval=task.get("approval"),
|
||||
model_provider=model_provider)
|
||||
|
||||
def goal_resume_task(self, task, goal_text=None,
|
||||
max_rounds=None, extra_env=None, model_provider=None) -> dict:
|
||||
def goal_resume_task(self, task: dict, goal_text: str | None = None,
|
||||
max_rounds: int | None = None,
|
||||
extra_env: dict | None = None,
|
||||
model_provider: str | None = None) -> dict:
|
||||
"""客内 host.py goal-resume:goal_text=None 为断点续跑
|
||||
(不带 --goal-text,沿用原 goal+verify),给 goal_text
|
||||
为开新目标(带 --goal-text 与 --max-rounds,默认 0)。"""
|
||||
@@ -2067,9 +2083,9 @@ class CloudHypervisorExecutor(Executor):
|
||||
effort=task.get("effort"), approval=task.get("approval"),
|
||||
model_provider=model_provider)
|
||||
|
||||
def goal_resume_breakpoint_task(self, task,
|
||||
model_provider=None,
|
||||
extra_env=None) -> dict:
|
||||
def goal_resume_breakpoint_task(self, task: dict,
|
||||
model_provider: str | None = None,
|
||||
extra_env: dict | None = None) -> dict:
|
||||
"""客内 host.py goal-resume 断点续跑:无 --goal-text、
|
||||
无 --max-rounds,host 沿用原任务从断点轮次续跑。
|
||||
worker 层已接线(补缺口轨):非网关的 CHV 断点任务
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
"""快照预热池 WarmPool(实验性,非生产路径,feat-chv-arch95 定案)。
|
||||
"""快照预热池 WarmPool(纯实验模块,非生产路径)。
|
||||
|
||||
本轨(feat-chv-arch-maint-fe)破环性清理:WarmPool 从
|
||||
cloud/chv/pool.py 移到本文件,cloud/chv/__init__ 与
|
||||
cloud/sandbox.py 不再 re-export 它,生产代码 import 本
|
||||
模块即违规(见 docs/chv-architecture.md ADR-12 的禁
|
||||
import 清单与测试钉住)。实验用法只走
|
||||
`from cloud.chv.experimental_pool import WarmPool`。
|
||||
生产残留清理已彻底分文件:见 cloud/chv/orphans.py 的
|
||||
reconcile_orphans_for_executor(),它不 import 本模块。
|
||||
|
||||
定案(架构决策,见 docs/chv-architecture.md ADR-5):WarmPool
|
||||
不再宣称为生产快速启动特性。原因:预热 VM 的磁盘是干净快照
|
||||
@@ -7,10 +16,11 @@
|
||||
语义三者都未实现),把预热盘直接当任务盘用会击穿“任务盘
|
||||
隔离”不变量。acquire 内 resume 只是实验性接口,worker
|
||||
dispatch 永远走冷启动(wake_vm),_run_with_vm 绝不查池。
|
||||
pool 保留的唯一生产价值是 reconcile_orphans()(上一世残留
|
||||
清理),worker 启动/关闭仍调它。warm_pool_wired_to_dispatch
|
||||
恒为 False,任何把它改成 True 的改动都必须先实现任务盘
|
||||
重供给并补不变量测试,否则不得合入。
|
||||
本模块的 reconcile_orphans() 只留给实验/旧测试直接调用;
|
||||
生产启动/关闭走 orphans facade,不再经本类。
|
||||
warm_pool_wired_to_dispatch 恒为 False,任何把它改成
|
||||
True 的改动都必须先实现任务盘重供给并补不变量测试,
|
||||
否则不得合入。
|
||||
|
||||
|
||||
隔离红线(与 #216 一致,绝不妥协):预热条目是一次性的
|
||||
@@ -43,13 +53,11 @@ Registry(本轨新增):
|
||||
仍可用,重启后孤儿改由 socket 扫描兜底)。
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
_LOGGER = logging.getLogger("cloud.cloud_hypervisor")
|
||||
|
||||
_REGISTRY_FILENAME = "warm-pool.json"
|
||||
from cloud.chv.orphans import _REGISTRY_FILENAME
|
||||
from cloud.chv.orphans import _read_registry
|
||||
from cloud.chv.orphans import _write_registry
|
||||
|
||||
|
||||
class WarmPool:
|
||||
@@ -73,8 +81,9 @@ class WarmPool:
|
||||
|
||||
wired_to_dispatch = False
|
||||
|
||||
def __init__(self, executor, size=None, snapshot_url=None,
|
||||
registry_path=None):
|
||||
def __init__(self, executor: object, size: int | None = None,
|
||||
snapshot_url: str | None = None,
|
||||
registry_path: str | None = None) -> None:
|
||||
self.executor = executor
|
||||
self.size = int(size if size is not None
|
||||
else getattr(executor, "warm_pool_size", 0))
|
||||
@@ -93,24 +102,19 @@ class WarmPool:
|
||||
self._load_registry()
|
||||
|
||||
# -- registry ------------------------------------------------------
|
||||
def _load_registry(self):
|
||||
def _load_registry(self) -> None:
|
||||
"""载入 registry:读到的条目一律标 orphan(见模块
|
||||
docstring,绝不直接当 warm 发给任务)。无文件时为空
|
||||
池;损坏时 quarantine 并从空开始。"""
|
||||
池;损坏时 quarantine 并从空开始。文件 IO 与损坏
|
||||
语义唯一来源是 cloud.chv.orphans(生产模块),本
|
||||
方法只做“条目 → 内存 orphan 态”的映射。"""
|
||||
if not self.registry_path:
|
||||
return
|
||||
try:
|
||||
with open(self.registry_path, "r") as fh:
|
||||
data = json.load(fh)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
except (OSError, ValueError):
|
||||
data, corrupt = _read_registry(self.registry_path)
|
||||
if corrupt:
|
||||
self.registry_corrupt = True
|
||||
self._quarantine_registry()
|
||||
return
|
||||
if not isinstance(data, dict):
|
||||
self.registry_corrupt = True
|
||||
self._quarantine_registry()
|
||||
if data is None:
|
||||
return
|
||||
for name, entry in data.items():
|
||||
if not isinstance(entry, dict) \
|
||||
@@ -125,22 +129,10 @@ class WarmPool:
|
||||
"snapshot_url": entry.get("snapshot_url"),
|
||||
}
|
||||
|
||||
def _quarantine_registry(self):
|
||||
"""把损坏的 registry 文件挪到 <path>.corrupt 旁(尽力
|
||||
而为),原路径不再留损坏数据给下一次载入。"""
|
||||
if not self.registry_path:
|
||||
return
|
||||
try:
|
||||
os.replace(self.registry_path,
|
||||
self.registry_path + ".corrupt")
|
||||
except OSError:
|
||||
_LOGGER.warning("chv_pool_registry_quarantine_failed "
|
||||
"path=%s", self.registry_path)
|
||||
|
||||
def _save_registry(self):
|
||||
def _save_registry(self) -> None:
|
||||
"""落盘当前条目(只写 warm/assigned 态;orphan 是
|
||||
待清理的上一世条目,reconcile 后即删,不写回)。
|
||||
写失败只记日志,不炸任务流。"""
|
||||
写失败只记日志,不炸任务流。落盘 IO 委托 orphans。"""
|
||||
if not self.registry_path:
|
||||
return
|
||||
data = {}
|
||||
@@ -156,23 +148,14 @@ class WarmPool:
|
||||
"task_id": task_id,
|
||||
"snapshot_url": entry.get("snapshot_url"),
|
||||
}
|
||||
try:
|
||||
directory = os.path.dirname(self.registry_path)
|
||||
if directory:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
tmp_path = self.registry_path + ".tmp"
|
||||
with open(tmp_path, "w") as fh:
|
||||
json.dump(data, fh)
|
||||
os.replace(tmp_path, self.registry_path)
|
||||
except OSError:
|
||||
_LOGGER.warning("chv_pool_registry_save_failed path=%s",
|
||||
self.registry_path)
|
||||
_write_registry(self.registry_path, data)
|
||||
|
||||
def reconcile_orphans(self) -> list:
|
||||
"""清理上一世残留条目:对每个 orphan/assigned 条目
|
||||
尽力 shutdown_vm(清 socket 与残留登记),条目清掉
|
||||
并落盘,返回已清理的 VM 名列表。worker 启动/清理时
|
||||
显式调它(见模块 docstring 的单活池假设)。"""
|
||||
"""清理本池内存里的 orphan/assigned 条目(实验入口)。
|
||||
|
||||
对每个条目尽力 shutdown_vm,条目清掉并落盘,返回
|
||||
已清理的 VM 名列表。生产启动/关闭不走本方法,走
|
||||
cloud.chv.orphans.reconcile_orphans_for_executor()。"""
|
||||
cleaned = []
|
||||
for name, entry in list(self._entries.items()):
|
||||
if entry["state"] not in ("orphan", "assigned"):
|
||||
@@ -204,7 +187,7 @@ class WarmPool:
|
||||
return sum(1 for e in self._entries.values()
|
||||
if e["state"] in ("orphan", "assigned"))
|
||||
|
||||
def prewarm(self, count=None):
|
||||
def prewarm(self, count: int | None = None) -> list:
|
||||
"""预热到目标数:已有 warm 条目不重复建;每个新条目
|
||||
用一个合成任务名 wake + pause(合成任务 id 为
|
||||
warm-<序号>,绝不与真实任务 id 撞名)。返回新增的
|
||||
@@ -237,7 +220,7 @@ class WarmPool:
|
||||
self._save_registry()
|
||||
return added
|
||||
|
||||
def acquire(self, task):
|
||||
def acquire(self, task: dict) -> dict:
|
||||
"""分配一个预热 VM 给任务(feat-chv-fix,P1-3 接线):
|
||||
|
||||
有 warm 条目时先调 executor.resume_vm() 把暂停的 VM
|
||||
@@ -285,7 +268,7 @@ class WarmPool:
|
||||
"pool_task": entry["task"]}
|
||||
return {"mode": "cold", "vm": None, "pool_task": None}
|
||||
|
||||
def release(self, task, pool_vm=None):
|
||||
def release(self, task: dict, pool_vm: str | None = None) -> bool:
|
||||
"""释放:无论 warm 还是 cold 路径,VM 一律销毁
|
||||
(executor.shutdown_vm),池条目清掉;绝不把用过
|
||||
的 VM 放回 warm 池。返回 True 表示确实销毁了一个
|
||||
@@ -304,13 +287,3 @@ class WarmPool:
|
||||
"warm_pool_release", task,
|
||||
"vm=%s destroyed=%s" % (pool_vm, destroyed))
|
||||
return destroyed
|
||||
|
||||
|
||||
def reconcile_orphans_for_executor(executor) -> list:
|
||||
"""生产路径唯一的 WarmPool 入口:只做孤儿清理。
|
||||
|
||||
worker 启动/关闭调它,不构造 WarmPool 类、不调
|
||||
prewarm/acquire/release——实验接口与生产路径彻底分离:
|
||||
生产代码 import 本函数即可,acquire/prewarm 只存在于
|
||||
实验用的 WarmPool 类与测试里。"""
|
||||
return WarmPool(executor).reconcile_orphans()
|
||||
@@ -183,8 +183,9 @@ def _is_within(directory, path):
|
||||
return resolve_inside(directory, rel) is not None
|
||||
|
||||
|
||||
def validate_snapshot_url(snapshot_url: str, state_dir: str, snapshot_dir=None,
|
||||
allow_remote=False):
|
||||
def validate_snapshot_url(snapshot_url: str, state_dir: str,
|
||||
snapshot_dir: str | None = None,
|
||||
allow_remote: bool = False) -> str | None:
|
||||
"""快照 URL 白名单(L2):
|
||||
|
||||
- file://:目标须在 state_dir 或 CHV_SNAPSHOT_DIR 内,
|
||||
|
||||
+11
-9
@@ -40,7 +40,7 @@ import shlex
|
||||
GUEST_ENV_PATH = "/etc/chv/guest-env"
|
||||
|
||||
|
||||
def host_key_paths(state_dir: str, vm_name: str) -> tuple:
|
||||
def host_key_paths(state_dir: str, vm_name: str) -> dict:
|
||||
"""该 VM 的 host key 文件路径(私钥/公钥/known_hosts)。"""
|
||||
base = os.path.join(state_dir, vm_name)
|
||||
return {
|
||||
@@ -59,7 +59,7 @@ def dirty_marker_path(state_dir: str, vm_name: str) -> str:
|
||||
return os.path.join(state_dir, vm_name, ".cleanup-dirty")
|
||||
|
||||
|
||||
def delete_host_private_key(state_dir: str, vm_name: str) -> None:
|
||||
def delete_host_private_key(state_dir: str, vm_name: str) -> str:
|
||||
"""删宿主侧 host 私钥文件(feat-chv-secfix,H5):seed
|
||||
盘建好后宿主私钥已无存在必要(客机已有私钥、宿主只需
|
||||
公钥 + known_hosts 做校验)。删失败即 OSError,由调用
|
||||
@@ -70,7 +70,7 @@ def delete_host_private_key(state_dir: str, vm_name: str) -> None:
|
||||
return path
|
||||
|
||||
|
||||
def known_hosts_line(port, public_key_text, host="127.0.0.1"):
|
||||
def known_hosts_line(port: int, public_key_text: str, host: str = "127.0.0.1") -> str:
|
||||
"""公钥文件内容转 known_hosts 行:只取前两列(类型+数据)。
|
||||
|
||||
公钥文件形如 'ssh-ed25519 AAAA... [comment]',known_hosts
|
||||
@@ -88,7 +88,7 @@ def seed_disk_path(state_dir: str, vm_name: str) -> str:
|
||||
return os.path.join(state_dir, vm_name, "seed.iso")
|
||||
|
||||
|
||||
def build_seed_disk(seed_dir: str, seed_disk: str, runner=None) -> None:
|
||||
def build_seed_disk(seed_dir: str, seed_disk: str, runner: object = None) -> str:
|
||||
"""把 seed 目录做成 CIDATA 卷标 ISO(feat-chv-fix,P0-1 的
|
||||
注入载体):genisoimage -volid CIDATA -joliet -rock。
|
||||
|
||||
@@ -121,7 +121,7 @@ def build_seed_disk(seed_dir: str, seed_disk: str, runner=None) -> None:
|
||||
return seed_disk
|
||||
|
||||
|
||||
def build_seed_user_data(private_key_text, public_key_text):
|
||||
def build_seed_user_data(private_key_text: str, public_key_text: str) -> str:
|
||||
"""cloud-init user-data:把 host key 对写进客机 /etc/ssh。
|
||||
|
||||
注意:user-data 本身含私钥,只写进 per-VM seed 目录
|
||||
@@ -156,8 +156,9 @@ def build_seed_user_data(private_key_text, public_key_text):
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def write_seed(seed_dir, vm_name, private_key_text, public_key_text,
|
||||
secret_env=None):
|
||||
def write_seed(seed_dir: str, vm_name: str, private_key_text: str,
|
||||
public_key_text: str,
|
||||
secret_env: dict | None = None) -> str:
|
||||
"""写 seed 目录:meta-data + user-data(均为 0600)。
|
||||
|
||||
返回 seed 目录路径。seed 目录本身不含已知主机之外的
|
||||
@@ -204,8 +205,9 @@ def write_seed(seed_dir, vm_name, private_key_text, public_key_text,
|
||||
return seed_dir
|
||||
|
||||
|
||||
def generate_host_keypair(state_dir, vm_name, runner=None,
|
||||
keygen="ssh-keygen"):
|
||||
def generate_host_keypair(state_dir: str, vm_name: str,
|
||||
runner: object = None,
|
||||
keygen: str = "ssh-keygen") -> dict:
|
||||
"""为该 VM 生成(或复用本 VM 已有的)host key 对。
|
||||
|
||||
同一 VM 重启/恢复时复用已有对(否则钉死的 known_hosts
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
"""CHV 上一世残留 VM 清理(生产路径唯一入口)。
|
||||
|
||||
本模块是叶子模块,只依赖标准库,绝不 import
|
||||
cloud.chv.experimental_pool(实验接口)与 cloud.sandbox。
|
||||
生产代码(worker 启动/关闭)只调
|
||||
reconcile_orphans_for_executor(),它直接读 registry 文件、
|
||||
对每个上一世条目调 executor.shutdown_vm(),不构造任何
|
||||
池对象、不调 prewarm/acquire/release。
|
||||
|
||||
Registry 语义与旧 WarmPool._load_registry 完全一致
|
||||
(feat-chv-arch95plus 的 fail-safe 口径,本轨只搬位置、
|
||||
不改语义):
|
||||
- 文件:state_dir/warm-pool.json,内容为
|
||||
{<合成名>: {"state", "vm", "task_id", "snapshot_url"}}。
|
||||
- 读到的 warm/assigned 条目一律当 orphan:旧 VMM 进程
|
||||
已随旧 worker 消失,无法验证它还是当初那个干净快照
|
||||
VM,绝不发给任务,只能清理。
|
||||
- 损坏 fail-safe:文件不是合法 JSON / 顶层不是 dict 时,
|
||||
整个文件 quarantine 成 <path>.corrupt,清理从空开始;
|
||||
单条目结构非法(无 vm、或 state 不在 warm/assigned)
|
||||
时只丢该条,不连坐其它条目。
|
||||
- 清理后落盘为空 dict(原子 tmp + rename);写失败只记
|
||||
日志警告,不炸 worker 启动/关闭。shutdown_vm 抛错时该
|
||||
条目仍计入已清理(旧 reconcile 语义:尽力清理、不因
|
||||
单条失败卡住整批),_emit 失败同样 best-effort。
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
_LOGGER = logging.getLogger("cloud.cloud_hypervisor")
|
||||
|
||||
_REGISTRY_FILENAME = "warm-pool.json"
|
||||
|
||||
|
||||
def registry_path_for_executor(executor: object) -> str | None:
|
||||
"""执行器的 registry 路径:state_dir 下的 warm-pool.json。
|
||||
|
||||
无 state_dir 时返回 None(调用方按“无残留”处理)。"""
|
||||
state_dir = getattr(executor, "state_dir", None)
|
||||
if not state_dir:
|
||||
return None
|
||||
return os.path.join(state_dir, _REGISTRY_FILENAME)
|
||||
|
||||
|
||||
def _quarantine_registry(path: str) -> None:
|
||||
"""把损坏的 registry 文件挪到 <path>.corrupt 旁(尽力而为)。"""
|
||||
try:
|
||||
os.replace(path, path + ".corrupt")
|
||||
except OSError:
|
||||
_LOGGER.warning("chv_pool_registry_quarantine_failed "
|
||||
"path=%s", path)
|
||||
|
||||
|
||||
def _read_registry(path: str) -> tuple[dict | None, bool]:
|
||||
"""读 registry 原始 dict:(data, corrupt)。
|
||||
|
||||
无文件 → (None, False);损坏 → quarantine 后 (None, True);
|
||||
顶层非 dict 同样算损坏。调用方按 corrupt 决定是否记
|
||||
registry_corrupt(实验池需要区分“无文件”与“损坏”)。"""
|
||||
try:
|
||||
with open(path, "r") as fh:
|
||||
data = json.load(fh)
|
||||
except FileNotFoundError:
|
||||
return None, False
|
||||
except (OSError, ValueError):
|
||||
_quarantine_registry(path)
|
||||
return None, True
|
||||
if not isinstance(data, dict):
|
||||
_quarantine_registry(path)
|
||||
return None, True
|
||||
return data, False
|
||||
|
||||
|
||||
def _write_registry(path: str, data: dict) -> None:
|
||||
"""原子落盘 registry(tmp + rename);写失败只记日志。"""
|
||||
try:
|
||||
directory = os.path.dirname(path)
|
||||
if directory:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
tmp_path = path + ".tmp"
|
||||
with open(tmp_path, "w") as fh:
|
||||
json.dump(data, fh)
|
||||
os.replace(tmp_path, path)
|
||||
except OSError:
|
||||
_LOGGER.warning("chv_pool_registry_save_failed path=%s", path)
|
||||
|
||||
|
||||
def _orphan_entries(data: dict) -> list[dict]:
|
||||
"""从原始 registry dict 里筛出可清理的上一世条目。
|
||||
|
||||
只留 state 为 warm/assigned 且有 vm 的条目;非法条目
|
||||
静默丢弃(不连坐)。返回条目为 {"vm", "task_id",
|
||||
"snapshot_url"} 的新 dict,不改原数据。"""
|
||||
entries: list[dict] = []
|
||||
for entry in data.values():
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
if not entry.get("vm"):
|
||||
continue
|
||||
if entry.get("state") not in ("warm", "assigned"):
|
||||
continue
|
||||
entries.append({
|
||||
"vm": entry["vm"],
|
||||
"task_id": entry.get("task_id"),
|
||||
"snapshot_url": entry.get("snapshot_url"),
|
||||
})
|
||||
return entries
|
||||
|
||||
|
||||
def reconcile_orphans_for_executor(executor: object) -> list[str]:
|
||||
"""生产路径唯一的残留清理入口:返回已清理的 VM 名列表。
|
||||
|
||||
只做清理:读 registry → 每个上一世条目尽力
|
||||
shutdown_vm → 落盘为空 → best-effort 审计事件。
|
||||
绝不构造 WarmPool、绝不调 prewarm/acquire/release,
|
||||
绝不把任何条目发给任务。无 registry 路径时返回 []。"""
|
||||
path = registry_path_for_executor(executor)
|
||||
if not path:
|
||||
return []
|
||||
data, _corrupt = _read_registry(path)
|
||||
if data is None:
|
||||
return []
|
||||
entries = _orphan_entries(data)
|
||||
cleaned: list[str] = []
|
||||
for entry in entries:
|
||||
vm = entry["vm"]
|
||||
try:
|
||||
executor.shutdown_vm(vm) # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
pass
|
||||
cleaned.append(vm)
|
||||
if cleaned:
|
||||
_write_registry(path, {})
|
||||
try:
|
||||
executor._emit( # type: ignore[attr-defined]
|
||||
"warm_pool_reconcile", {"id": None},
|
||||
"cleaned=%s" % ",".join(cleaned))
|
||||
except Exception:
|
||||
pass
|
||||
return cleaned
|
||||
@@ -63,8 +63,9 @@ def split_secret_env(extra_env: dict) -> tuple:
|
||||
return secrets, plain
|
||||
|
||||
|
||||
def remote_command(inner_cmd, egress_proxy=None, extra_env=None,
|
||||
guest_env_file=None):
|
||||
def remote_command(inner_cmd: list, egress_proxy: str | None = None,
|
||||
extra_env: dict | None = None,
|
||||
guest_env_file: str | None = None) -> list:
|
||||
"""构造远程 shell 命令的 token 列表(不含 ssh 前缀)。
|
||||
|
||||
每个 token 都经 shlex.quote:ssh 客户端把 argv 用空格拼
|
||||
@@ -108,9 +109,12 @@ def known_hosts_path(state_dir: str, vm_name: str) -> str:
|
||||
return os.path.join(state_dir, vm_name, "known_hosts")
|
||||
|
||||
|
||||
def build_ssh_command(ssh_port, ssh_key, ssh_user, egress_proxy,
|
||||
known_hosts_file, inner_cmd, ssh_host="127.0.0.1",
|
||||
extra_env=None, guest_env_file=None):
|
||||
def build_ssh_command(ssh_port: int, ssh_key: str | None,
|
||||
ssh_user: str, egress_proxy: str | None,
|
||||
known_hosts_file: str, inner_cmd: list,
|
||||
ssh_host: str = "127.0.0.1",
|
||||
extra_env: dict | None = None,
|
||||
guest_env_file: str | None = None) -> list:
|
||||
"""构造 SSH 命令:ssh -p <端口> -i <key 文件> + 钉死选项
|
||||
+ user@<ssh_host> + 客内命令(远程 token 均经 shlex.quote,
|
||||
见 remote_command)。
|
||||
@@ -138,7 +142,7 @@ def build_ssh_command(ssh_port, ssh_key, ssh_user, egress_proxy,
|
||||
"%s@%s" % (ssh_user, ssh_host)] + remote
|
||||
|
||||
|
||||
def parse_keyscan_output(text, host="127.0.0.1"):
|
||||
def parse_keyscan_output(text: str, host: str = "127.0.0.1") -> list:
|
||||
"""从 ssh-keyscan 的 stdout 里挑出目标主机的 host key
|
||||
行:形如 "[<host>]:<port> ssh-ed25519 AAAA..."(-p 时
|
||||
的标准写法)或 "<host> ssh-rsa AAAA..."。其它主机行、
|
||||
|
||||
Reference in new issue
Block a user