feat: CHV gap fill — worker breakpoint wiring, pool reconcile, per-VM host keys, egress validation, guest build script
Stack: #216 -> #221 -> this. Worker breakpoint branch now calls any executor providing goal_resume_breakpoint_task (Local keeps its original path, gateway tasks still loud-fail); WarmPool reconcile_orphans is called on CHV worker startup and close with audit-on-failure; per-VM SSH host keys are provisioned before VMM start via cloud-init NoCloud seed (keyscan kept only as weak fallback, CHV_HOST_KEY_MODE=provisioned|keyscan); CHV_NET_JSON is validated in preflight and explicit direct egress is audited; chv-guest/build.sh is now real steps with --dry-run and fails fast without /dev/kvm. Tests: new test_chv_gaps.py (18), cloud full 1514 OK (skipped=4). No real VM verification: no KVM host.
This commit is contained in:
1 parent
fde7bf4c3e
commit
22b83f118a
8 files changed
+922
-59
No files matched your search
+35
-11
@@ -1,11 +1,12 @@
|
||||
# Cloud Hypervisor 客机镜像(chv-guest)——内容清单与版本化构建说明
|
||||
|
||||
> 本目录只有清单与构建脚本骨架,镜像本身不在本轨内实际构建
|
||||
> 发布。原因写在前面,不藏着:当前没有带 /dev/kvm 的宿主
|
||||
> 可以构建并实测 VM 镜像(生产主机是 CVM、无嵌套虚拟化;
|
||||
> 本 VM 容器内同样无 /dev/kvm)。构建与实测见
|
||||
> `docs/chv-production-readiness.md` 的“需镜像构建 / 需 KVM
|
||||
> 宿主实测”项,绝不在本轨声称镜像已建成、已发布、已跑通。
|
||||
> 本目录有清单与真实构建步骤(build.sh,补缺口轨已实做),
|
||||
> 但镜像本身不在本轨内实际构建发布。原因写在前面,不藏着:
|
||||
> 当前没有带 /dev/kvm 的宿主可以构建并实测 VM 镜像(生产
|
||||
> 主机是 CVM、无嵌套虚拟化;本 VM 容器内同样无 /dev/kvm)。
|
||||
> 构建与实测见 `docs/chv-production-readiness.md` 的“需镜像
|
||||
> 构建 / 需 KVM 宿主实测”项,绝不在本轨声称镜像已建成、
|
||||
> 已发布、已跑通。
|
||||
|
||||
## 1. 镜像定位
|
||||
|
||||
@@ -53,12 +54,35 @@ chv-guest-<ubuntu 版本>-<codex 版本>-<仓库 commit 短 hash>
|
||||
- `BUILDINFO.json`:`{ubuntu, codex_version, repo_commit, built_at,
|
||||
contents: [清单逐项版本]}`
|
||||
|
||||
## 4. 构建脚本骨架
|
||||
## 4. 构建脚本(补缺口轨:已从骨架改成真实步骤)
|
||||
|
||||
见 `build.sh`:只做参数校验、产物路径与版本 tag 计算、以及
|
||||
“需要 KVM 宿主 / virt-customize 才能执行的步骤”占位。脚本在
|
||||
无 KVM 的机器上运行时应在第一步就明确退出(`CHV_GUEST_BUILD
|
||||
needs a KVM host`),绝不假装构建成功。
|
||||
见 `build.sh`:参数校验 → 下载 Ubuntu 云镜像 → qemu-img
|
||||
转 raw → virt-customize 装 §2 清单、拷入 codex 成套二进制
|
||||
与仓库代码到 /app、写 host key 不重生成配置 →
|
||||
virt-get-kernel 取内核 → 生成并校验 BUILDINFO.json。
|
||||
`./build.sh --dry-run out/` 只打印命令、不执行、不要求
|
||||
KVM;全量模式在无 /dev/kvm 的机器上第一步就明确退出
|
||||
(`CHV_GUEST_BUILD needs a KVM host`),绝不假装构建成功。
|
||||
本轨未在真机上跑过全量模式,镜像未建成、未发布,仍属
|
||||
“需 KVM 宿主实测”项。
|
||||
|
||||
## 6. Host key 首启注入硬性要求(补缺口轨新增)
|
||||
|
||||
执行器的 per-VM 预置方案(见 `cloud/chv/hostkeys.py`)能
|
||||
跑通的前提是客机镜像必须满足以下全部,否则预置在真机
|
||||
上跑不通,执行器会 fail-fast,不回落成弱方案:
|
||||
- 装 cloud-init 且支持 NoCloud seed(seed 磁盘/目录由
|
||||
执行器在 VM 启动前生成,含 meta-data 与 user-data);
|
||||
- 首启时先应用 seed 再启动 sshd,seed 里的 user-data
|
||||
把该 VM 的 host key 对写进 /etc/ssh/ 并重启 sshd;
|
||||
- 镜像里已钉死 `ssh_deletekeys: false` 与
|
||||
`ssh_genkeytypes: []`(build.sh 已写),首启不得无条件
|
||||
重新生成 host key——重生成会使预置的 known_hosts 立即
|
||||
失效,SSH 钉死直接拒绝,这是设计,不是 bug;
|
||||
- sshd 只允许 key 登录,禁止密码登录;
|
||||
- seed 磁盘/目录是 per-VM 敏感物,执行器在 VM 关闭/
|
||||
回收时删除,镜像构建方不得把任何一把 host key 固化
|
||||
进镜像(所有 VM 同 key 是安全事故)。
|
||||
|
||||
## 5. 与执行器的对接
|
||||
|
||||
|
||||
+110
-19
@@ -1,26 +1,52 @@
|
||||
#!/bin/bash
|
||||
# chv-guest 镜像构建脚本骨架(未在无 KVM 机器上实际执行过)。
|
||||
# chv-guest 镜像构建脚本(补缺口轨:从 TODO 骨架改成真实步骤)。
|
||||
#
|
||||
# 用法(需要带 /dev/kvm 的宿主,见 README):
|
||||
# 用法(必须在带 /dev/kvm 的宿主上跑全量模式):
|
||||
# CODEX_BIN_DIR=bin UBUNTU_VERSION=24.04 CODEX_VERSION=0.164.0-alpha.9 \
|
||||
# REPO_COMMIT=$(git rev-parse --short HEAD) ./build.sh out/
|
||||
# ./build.sh --dry-run out/ # 只打印命令,不执行、不要求 KVM
|
||||
#
|
||||
# 本脚本只做三件事,绝不假装已建成镜像:
|
||||
# 1. 参数校验(缺 CODEX_BIN_DIR 成套二进制、缺版本、缺 /dev/kvm
|
||||
# 即退出,退出码非 0);
|
||||
# 2. 算出版本 tag(chv-guest-<ubuntu>-<codex>-<commit>);
|
||||
# 3. 列出仍需在 KVM 宿主上执行的步骤(下载 Ubuntu 云镜像、
|
||||
# virt-customize 装清单软件、拷入 /app 代码、生成 vmlinux /
|
||||
# disk.raw / BUILDINFO.json),每一步在骨架里以 TODO 标记,
|
||||
# 不在本轨内静默跳过或谎称完成。
|
||||
# 产物(out/ 内):
|
||||
# vmlinux # virt-get-kernel 从云镜像取出的内核(CHV --kernel 指向它)
|
||||
# disk.raw # qemu-img 转成的 raw 根文件系统镜像(CHV_ROOTFS 指向它)
|
||||
# BUILDINFO.json # {tag, ubuntu, codex_version, repo_commit, built_at, contents}
|
||||
# 绝不进镜像的东西:任何模型 key、系统 key、用户账号数据、
|
||||
# 生产凭证;模型 key 由客机 cloud-init 预置的运行环境变量名
|
||||
# 注入,不写进镜像层。
|
||||
#
|
||||
# 客机 host key 硬性要求(见 README §6,补缺口轨新增):
|
||||
# 客机必须装 cloud-init 且支持 NoCloud seed,首启先应用
|
||||
# seed 再启动 sshd,且不得无条件重新生成 host key,否则
|
||||
# 执行器的 per-VM 预置钉死即失效。本脚本在镜像里写
|
||||
# /etc/cloud/cloud.cfg.d/99-chv-hostkey.cfg(ssh_deletekeys:
|
||||
# false、ssh_genkeytypes: []),把“不重生成”钉进镜像。
|
||||
set -eu
|
||||
|
||||
DRY_RUN=0
|
||||
if [ "${1:-}" = "--dry-run" ]; then
|
||||
DRY_RUN=1
|
||||
shift
|
||||
fi
|
||||
OUT="${1:-out}"
|
||||
: "${CODEX_BIN_DIR:?CODEX_BIN_DIR 未设:需 npm 安装 @openai/codex 后的配对二进制目录}"
|
||||
: "${UBUNTU_VERSION:?UBUNTU_VERSION 未设}"
|
||||
: "${CODEX_VERSION:?CODEX_VERSION 未设}"
|
||||
: "${REPO_COMMIT:?REPO_COMMIT 未设:构建时仓库 commit 短 hash}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_DIR="${REPO_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
||||
TAG="chv-guest-${UBUNTU_VERSION}-${CODEX_VERSION}-${REPO_COMMIT}"
|
||||
UBUNTU_IMAGE_URL="${UBUNTU_IMAGE_URL:-https://cloud-images.ubuntu.com/releases/${UBUNTU_VERSION}/release/ubuntu-${UBUNTU_VERSION}-server-cloudimg-amd64.img}"
|
||||
|
||||
run() {
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
echo "DRY-RUN: $*"
|
||||
else
|
||||
echo "+ $*"
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# 参数校验(干跑与全量都做,不依赖 KVM)。
|
||||
for bin in codex codex-code-mode-host; do
|
||||
if [ ! -x "$CODEX_BIN_DIR/$bin" ]; then
|
||||
echo "缺少配对二进制:$CODEX_BIN_DIR/$bin,构建失败" >&2
|
||||
@@ -31,17 +57,82 @@ if [ ! -d "$CODEX_BIN_DIR/codex-resources" ]; then
|
||||
echo "缺少 codex-resources(bwrap 载体),构建失败" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -e /dev/kvm ]; then
|
||||
echo "CHV_GUEST_BUILD needs a KVM host: /dev/kvm 不存在" >&2
|
||||
if [ ! -f "$REPO_DIR/host.py" ] || [ ! -d "$REPO_DIR/cloud" ]; then
|
||||
echo "REPO_DIR 不是 codex-app-server 仓库根(缺 host.py/cloud/):$REPO_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$DRY_RUN" = "0" ]; then
|
||||
if [ ! -e /dev/kvm ]; then
|
||||
echo "CHV_GUEST_BUILD needs a KVM host: /dev/kvm 不存在" >&2
|
||||
exit 1
|
||||
fi
|
||||
for tool in curl qemu-img virt-customize virt-get-kernel python3; do
|
||||
if ! command -v "$tool" >/dev/null 2>&1; then
|
||||
echo "缺少构建工具:$tool,构建失败" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
TAG="chv-guest-${UBUNTU_VERSION}-${CODEX_VERSION}-${REPO_COMMIT}"
|
||||
mkdir -p "$OUT"
|
||||
run mkdir -p "$OUT"
|
||||
IMG="$OUT/ubuntu-${UBUNTU_VERSION}-server-cloudimg-amd64.img"
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
echo "DRY-RUN: curl -fL $UBUNTU_IMAGE_URL -o $IMG"
|
||||
echo "DRY-RUN: [ -s $IMG ] || exit 1"
|
||||
else
|
||||
if [ ! -s "$IMG" ]; then
|
||||
curl -fL "$UBUNTU_IMAGE_URL" -o "$IMG"
|
||||
fi
|
||||
[ -s "$IMG" ] || { echo "下载的云镜像为空:$IMG" >&2; exit 1; }
|
||||
if [ -n "${UBUNTU_IMAGE_SHA256:-}" ]; then
|
||||
echo "$UBUNTU_IMAGE_SHA256 $IMG" | sha256sum -c -
|
||||
fi
|
||||
fi
|
||||
run qemu-img convert -f qcow2 -O raw "$IMG" "$OUT/disk.raw"
|
||||
# 清单软件(照抄 cloud/docker/Dockerfile 的 apt/pip 清单,
|
||||
# 中文 LaTeX xelatex+CJK 故意不装,见 README;Playwright
|
||||
# 默认不装,PLAYWRIGHT=1 时才加装,属可选步骤)。
|
||||
run virt-customize -a "$OUT/disk.raw" \
|
||||
--install git,pandoc,poppler-utils,latexmk,texlive-latex-base,openssh-server,cloud-init,qemu-guest-agent,python3,python3-pip \
|
||||
--run-command 'pip3 install --no-cache-dir --break-system-packages python-docx openpyxl python-pptx PyMuPDF Pillow || pip3 install --no-cache-dir python-docx openpyxl python-pptx PyMuPDF Pillow' \
|
||||
--copy-in "$CODEX_BIN_DIR/codex:/usr/local/bin/" \
|
||||
--copy-in "$CODEX_BIN_DIR/codex-code-mode-host:/usr/local/bin/" \
|
||||
--copy-in "$CODEX_BIN_DIR/codex-resources:/usr/local/bin/" \
|
||||
--run-command 'chmod +x /usr/local/bin/codex /usr/local/bin/codex-code-mode-host; ln -sf /usr/local/bin/codex-resources/bwrap /usr/local/bin/bwrap' \
|
||||
--mkdir /app \
|
||||
--copy-in "$REPO_DIR/host.py:/app/" \
|
||||
--copy-in "$REPO_DIR/cloud:/app/" \
|
||||
--run-command 'printf "ssh_deletekeys: false\nssh_genkeytypes: []\n" > /etc/cloud/cloud.cfg.d/99-chv-hostkey.cfg; systemctl enable ssh || true'
|
||||
if [ "${PLAYWRIGHT:-0}" = "1" ]; then
|
||||
run virt-customize -a "$OUT/disk.raw" \
|
||||
--run-command 'pip3 install --no-cache-dir playwright; python3 -m playwright install --with-deps chromium'
|
||||
fi
|
||||
run virt-get-kernel -a "$OUT/disk.raw" -o "$OUT/vmlinux"
|
||||
# 产物校验(干跑只打印,不执行校验命令的副作用部分)。
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
echo "DRY-RUN: test -s $OUT/vmlinux && test -s $OUT/disk.raw"
|
||||
echo "DRY-RUN: python3 生成 $OUT/BUILDINFO.json 并校验 tag=$TAG"
|
||||
else
|
||||
test -s "$OUT/vmlinux" || { echo "内核产物为空:$OUT/vmlinux" >&2; exit 1; }
|
||||
test -s "$OUT/disk.raw" || { echo "磁盘产物为空:$OUT/disk.raw" >&2; exit 1; }
|
||||
python3 - "$OUT/BUILDINFO.json" "$TAG" "$UBUNTU_VERSION" "$CODEX_VERSION" "$REPO_COMMIT" <<'PYEOF'
|
||||
import json, sys, time
|
||||
path, tag, ubuntu, codex, commit = sys.argv[1:6]
|
||||
info = {"tag": tag, "ubuntu": ubuntu, "codex_version": codex,
|
||||
"repo_commit": commit, "built_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"contents": ["git", "pandoc", "poppler-utils", "latexmk", "texlive-latex-base",
|
||||
"openssh-server", "cloud-init", "qemu-guest-agent",
|
||||
"python-docx", "openpyxl", "python-pptx", "PyMuPDF", "Pillow",
|
||||
"codex", "codex-code-mode-host", "codex-resources/bwrap", "/app/host.py"]}
|
||||
with open(path, "w") as fh:
|
||||
json.dump(info, fh, indent=2)
|
||||
PYEOF
|
||||
python3 -c "import json; d=json.load(open('$OUT/BUILDINFO.json')); assert d['tag'] == '$TAG', d"
|
||||
fi
|
||||
echo "tag=$TAG"
|
||||
echo "out=$OUT"
|
||||
echo "TODO(KVM host): 下载 Ubuntu ${UBUNTU_VERSION} 云镜像并转 raw"
|
||||
echo "TODO(KVM host): virt-customize 装 README §2 清单(git/python/pandoc/poppler/latex/codex/playwright/openssh)"
|
||||
echo "TODO(KVM host): 拷入仓库代码到 /app(版本 $REPO_COMMIT),绝不拷入任何 key"
|
||||
echo "TODO(KVM host): 生成 vmlinux + disk.raw + BUILDINFO.json,校验 tag 与清单版本"
|
||||
echo "构建骨架结束:以上 TODO 步骤未执行,镜像未建成(见 README 与生产就绪清单)"
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
echo "干跑结束:未执行任何构建步骤,镜像未建成"
|
||||
else
|
||||
echo "构建完成:$OUT/vmlinux $OUT/disk.raw $OUT/BUILDINFO.json"
|
||||
fi
|
||||
Reference in new issue
Block a user