fix(chv-guest): 客机镜像构建缺口补齐(B-02/B-03/B-04/B-05/B-06/B-07/B-08/B-09/B-10/B-13/B-15)
- B-10(实测):virt-customize chroot 里 sshd -t 必败,双因: 无 host key(exit 1)+ 缺 /run/sshd(exit 255);修为 mkdir -p /run/sshd + ssh-keygen -A 临时生成,断言通过后 立删临时 key,镜像仍不带 host key(已在 WSL 真机预演验证) - B-08:Playwright 装在第二次定制,须重写 pip freeze 否则 BUILDINFO 里无 playwright;Chromium 以 ms-playwright 缓存 目录名记录进 guest-versions-chromium.txt - B-09:codex --version 须含 CODEX_VERSION;REPO_COMMIT 须 等于 HEAD 短 hash;工作树脏即拒绝构建 - B-02/B-13:BUILDINFO 生成抽成 buildinfo.py(可单元测试), 补 disk.raw/vmlinux sha256+大小、ubuntu 镜像 sha、cmdline、 playwright 开关、image_sha256 字段 - generate_lockfile.py 改写:版本唯一来源改为 BUILDINFO.versions(客机读回),codex 三件套取 codex_version、/app/host.py 取 repo_commit、chromium 取 versions.chromium;不再查构建宿主 dpkg/pip(旧方法不成立) - check_lockfile.py 补强:条目格式、contents 全覆盖、冻结 清单双向比对 - B-03/B-04 输入格式校验、B-05 curl 重试、B-06 哈希不符 删文件重下、B-07 磁盘预检、B-15 set -euo pipefail - 同步改 test_chv_to10.py:generate 不再查宿主,改断言客机 版本来源 + 不回落宿主
This commit is contained in:
1 parent
8c8d62f70d
commit
7f18c2d13b
5 files changed
+372
-96
No files matched your search
+67
-32
@@ -19,6 +19,10 @@
|
||||
# sshd 加固由构建写入 /etc/ssh/sshd_config.d/99-chv.conf
|
||||
# 并经 sshd -t + grep 断言(README 的“只允许 key 登录”
|
||||
# 从此是构建断言,不再只是文档声称)。
|
||||
# B-10:virt-customize 的 chroot 里 sshd -t 须先 ssh-keygen -A
|
||||
# 临时生成 host key 并 mkdir -p /run/sshd,否则分别报无 host
|
||||
# key / 缺 privilege separation 目录而失败;断言通过后立即删
|
||||
# 临时 key,镜像仍不带 host key(per-VM 预置才生效)。
|
||||
#
|
||||
# 客机 host key 硬性要求(见 README §5,补缺口轨新增):
|
||||
# 客机必须装 cloud-init 且支持 NoCloud seed,首启先应用
|
||||
@@ -26,7 +30,7 @@
|
||||
# 执行器的 per-VM 预置钉死即失效。本脚本在镜像里写
|
||||
# /etc/cloud/cloud.cfg.d/99-chv-hostkey.cfg(ssh_deletekeys:
|
||||
# false、ssh_genkeytypes: []),把“不重生成”钉进镜像。
|
||||
set -eu
|
||||
set -euo pipefail
|
||||
|
||||
DRY_RUN=0
|
||||
if [ "${1:-}" = "--dry-run" ]; then
|
||||
@@ -40,6 +44,35 @@ OUT="${1:-out}"
|
||||
: "${REPO_COMMIT:?REPO_COMMIT 未设:构建时仓库 commit 短 hash}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_DIR="${REPO_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
||||
# B-03/B-04:输入格式校验(干跑与全量都做,报错点提前,
|
||||
# 不等到 curl 404 或 sha256sum -c 才失败)。
|
||||
if ! printf '%s' "$UBUNTU_VERSION" | grep -Eq '^[0-9]+\.[0-9]+$'; then
|
||||
echo "UBUNTU_VERSION 格式须为 NN.NN:$UBUNTU_VERSION" >&2; exit 1
|
||||
fi
|
||||
if ! printf '%s' "$REPO_COMMIT" | grep -Eq '^[0-9a-f]{7,40}$'; then
|
||||
echo "REPO_COMMIT 须为 7-40 位 hex:$REPO_COMMIT" >&2; exit 1
|
||||
fi
|
||||
if ! printf '%s' "$CODEX_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+'; then
|
||||
echo "CODEX_VERSION 格式异常:$CODEX_VERSION" >&2; exit 1
|
||||
fi
|
||||
if [ -n "${UBUNTU_IMAGE_SHA256:-}" ] && ! printf '%s' "$UBUNTU_IMAGE_SHA256" | grep -Eq '^[0-9a-f]{64}$'; then
|
||||
echo "UBUNTU_IMAGE_SHA256 须为 64 位 hex" >&2; exit 1
|
||||
fi
|
||||
# B-09:版本声称须核对,不只进 tag。codex 二进制真实版本
|
||||
# 须包含 CODEX_VERSION;REPO_COMMIT 须等于仓库 HEAD 短 hash;
|
||||
# 工作树脏即拒绝构建(tag 会谎称干净 commit)。
|
||||
CODEX_ACTUAL_VERSION="$("$CODEX_BIN_DIR/codex" --version 2>/dev/null || true)"
|
||||
case "$CODEX_ACTUAL_VERSION" in
|
||||
*"$CODEX_VERSION"*) ;;
|
||||
*) echo "codex 二进制版本与 CODEX_VERSION 不符:got [$CODEX_ACTUAL_VERSION] want 含 [$CODEX_VERSION]" >&2; exit 1 ;;
|
||||
esac
|
||||
HEAD_SHORT="$(git -C "$REPO_DIR" rev-parse --short HEAD)"
|
||||
if [ "$HEAD_SHORT" != "$REPO_COMMIT" ]; then
|
||||
echo "REPO_COMMIT 与仓库 HEAD 不符:$REPO_COMMIT != $HEAD_SHORT" >&2; exit 1
|
||||
fi
|
||||
if [ -n "$(git -C "$REPO_DIR" status --porcelain)" ]; then
|
||||
echo "仓库工作树有未提交修改,拒绝构建(tag 会谎称干净 commit)" >&2; exit 1
|
||||
fi
|
||||
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}"
|
||||
|
||||
@@ -81,6 +114,13 @@ if [ "$DRY_RUN" = "0" ]; then
|
||||
fi
|
||||
|
||||
run mkdir -p "$OUT"
|
||||
# B-07:磁盘空间预检(仅全量模式;干跑不建目录,无法 df)。
|
||||
if [ "$DRY_RUN" = "0" ]; then
|
||||
avail_kb="$(df -k --output=avail "$OUT" | tail -1 | tr -d ' ')"
|
||||
if [ "${avail_kb:-0}" -lt 20971520 ]; then
|
||||
echo "磁盘不足:$OUT 所在盘可用 ${avail_kb}KB,需 ≥20GB" >&2; exit 1
|
||||
fi
|
||||
fi
|
||||
IMG="$OUT/ubuntu-${UBUNTU_VERSION}-server-cloudimg-amd64.img"
|
||||
# feat-chv-secfix(M4):云镜像 SHA256 改为必填(干跑只验
|
||||
# “已设”,不验文件;全量模式下载后验哈希)。未设即失败,
|
||||
@@ -90,15 +130,22 @@ if [ -z "${UBUNTU_IMAGE_SHA256:-}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
echo "DRY-RUN: curl -fL $UBUNTU_IMAGE_URL -o $IMG"
|
||||
echo "DRY-RUN: curl -fL --retry 3 --retry-all-errors --connect-timeout 30 $UBUNTU_IMAGE_URL -o $IMG"
|
||||
echo "DRY-RUN: [ -s $IMG ] || exit 1"
|
||||
echo "DRY-RUN: sha256sum -c (UBUNTU_IMAGE_SHA256)"
|
||||
else
|
||||
if [ ! -s "$IMG" ]; then
|
||||
curl -fL "$UBUNTU_IMAGE_URL" -o "$IMG"
|
||||
curl -fL --retry 3 --retry-all-errors --connect-timeout 30 "$UBUNTU_IMAGE_URL" -o "$IMG"
|
||||
fi
|
||||
[ -s "$IMG" ] || { echo "下载的云镜像为空:$IMG" >&2; exit 1; }
|
||||
echo "$UBUNTU_IMAGE_SHA256 $IMG" | sha256sum -c -
|
||||
# B-06:哈希不符时删文件重下一次,不直接退出让操作员手动清理。
|
||||
if ! echo "$UBUNTU_IMAGE_SHA256 $IMG" | sha256sum -c -; then
|
||||
echo "云镜像哈希不符,删除后重下一次:$IMG" >&2
|
||||
rm -f "$IMG"
|
||||
curl -fL --retry 3 --retry-all-errors --connect-timeout 30 "$UBUNTU_IMAGE_URL" -o "$IMG"
|
||||
[ -s "$IMG" ] || { echo "重下的云镜像为空:$IMG" >&2; exit 1; }
|
||||
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 清单,
|
||||
@@ -115,11 +162,18 @@ run virt-customize -a "$OUT/disk.raw" \
|
||||
--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' \
|
||||
--run-command 'printf "PasswordAuthentication no\nPermitRootLogin prohibit-password\nChallengeResponseAuthentication no\nKbdInteractiveAuthentication no\nPubkeyAuthentication yes\n" > /etc/ssh/sshd_config.d/99-chv.conf; sshd -t && grep -q "^PasswordAuthentication no" /etc/ssh/sshd_config.d/99-chv.conf && grep -q "^PermitRootLogin prohibit-password" /etc/ssh/sshd_config.d/99-chv.conf' \
|
||||
--run-command 'printf "PasswordAuthentication no\nPermitRootLogin prohibit-password\nChallengeResponseAuthentication no\nKbdInteractiveAuthentication no\nPubkeyAuthentication yes\n" > /etc/ssh/sshd_config.d/99-chv.conf && mkdir -p /run/sshd && ssh-keygen -A && sshd -t && grep -q "^PasswordAuthentication no" /etc/ssh/sshd_config.d/99-chv.conf && grep -q "^PermitRootLogin prohibit-password" /etc/ssh/sshd_config.d/99-chv.conf && rm -f /etc/ssh/ssh_host_* && test -z "$(ls /etc/ssh/ssh_host_* 2>/dev/null)"' \
|
||||
--run-command 'dpkg-query -W -f "${Package} ${Version}\n" > /app/guest-versions-dpkg.txt; pip3 freeze > /app/guest-versions-pip.txt || 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'
|
||||
# B-08:pip freeze 是在第一次定制时写的,Playwright 装在第二次
|
||||
# 定制,须重写 freeze,否则 BUILDINFO.versions.pip 里没有
|
||||
# playwright;Chromium 版本以 ms-playwright 缓存目录名记录
|
||||
# (如 chromium-1200),写进 /app/guest-versions-chromium.txt。
|
||||
run virt-customize -a "$OUT/disk.raw" \
|
||||
--run-command 'dpkg-query -W -f "${Package} ${Version}\n" > /app/guest-versions-dpkg.txt; pip3 freeze > /app/guest-versions-pip.txt || true' \
|
||||
--run-command 'ls /root/.cache/ms-playwright/ > /app/guest-versions-chromium.txt 2>/dev/null || true'
|
||||
fi
|
||||
run virt-get-kernel -a "$OUT/disk.raw" -o "$OUT/vmlinux"
|
||||
# feat-chv-secfix(M4):构建时把客机内实际装好的版本
|
||||
@@ -131,43 +185,24 @@ run virt-get-kernel -a "$OUT/disk.raw" -o "$OUT/vmlinux"
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
echo "DRY-RUN: virt-cat -a $OUT/disk.raw /app/guest-versions-dpkg.txt > $OUT/guest-versions-dpkg.txt"
|
||||
echo "DRY-RUN: virt-cat -a $OUT/disk.raw /app/guest-versions-pip.txt > $OUT/guest-versions-pip.txt"
|
||||
if [ "${PLAYWRIGHT:-0}" = "1" ]; then
|
||||
echo "DRY-RUN: virt-cat -a $OUT/disk.raw /app/guest-versions-chromium.txt > $OUT/guest-versions-chromium.txt"
|
||||
fi
|
||||
else
|
||||
virt-cat -a "$OUT/disk.raw" /app/guest-versions-dpkg.txt > "$OUT/guest-versions-dpkg.txt"
|
||||
virt-cat -a "$OUT/disk.raw" /app/guest-versions-pip.txt > "$OUT/guest-versions-pip.txt"
|
||||
if [ "${PLAYWRIGHT:-0}" = "1" ]; then
|
||||
virt-cat -a "$OUT/disk.raw" /app/guest-versions-chromium.txt > "$OUT/guest-versions-chromium.txt" || true
|
||||
fi
|
||||
fi
|
||||
# 产物校验(干跑只打印,不执行校验命令的副作用部分)。
|
||||
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"
|
||||
echo "DRY-RUN: python3 $SCRIPT_DIR/buildinfo.py $OUT $TAG $UBUNTU_VERSION $CODEX_VERSION $REPO_COMMIT <sha256> ${PLAYWRIGHT:-0}"
|
||||
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]
|
||||
def _read_lines(p):
|
||||
try:
|
||||
with open(p) as fh:
|
||||
return [l.strip() for l in fh if l.strip()]
|
||||
except OSError:
|
||||
return []
|
||||
import os
|
||||
outdir = os.path.dirname(path)
|
||||
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"],
|
||||
"versions": {
|
||||
"dpkg": _read_lines(os.path.join(outdir, "guest-versions-dpkg.txt")),
|
||||
"pip": _read_lines(os.path.join(outdir, "guest-versions-pip.txt")),
|
||||
},
|
||||
"ubuntu_image_sha256_set": True}
|
||||
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; assert d['versions']['dpkg'], 'BUILDINFO versions.dpkg 为空'; assert d['versions']['pip'], 'BUILDINFO versions.pip 为空'"
|
||||
python3 "$SCRIPT_DIR/buildinfo.py" "$OUT" "$TAG" "$UBUNTU_VERSION" "$CODEX_VERSION" "$REPO_COMMIT" "$UBUNTU_IMAGE_SHA256" "${PLAYWRIGHT:-0}"
|
||||
fi
|
||||
echo "tag=$TAG"
|
||||
echo "out=$OUT"
|
||||
|
||||
Reference in new issue
Block a user