ci / test (push) Failing after 47m51s
第 2 轮复测(0dc8328)p2 仍 FAIL 的两处根因: 1. 挂盘时序竞态:add-disk 返回 06:59:10,root hotplug 路径 (cloud-init-hotplugd → cloud-final runcmd → chv-data-mount) 06:59:14–16 才挂上;旧 attach 以 chv 调 chv-data-mount 并判 rc==0,但 /dev/vd? 属 root:disk、chv 的 blkid/mkfs 必败, 脚本又静默 exit 0,rc 检查是空洞的成功假象。 2. 卸载权限:挂载是 root 做的,chv 的 plain umount 恒 rc=32 must be superuser,release 按现写法永远失败、VM 留置。 修法选 (a) 轮询,不选 (b) root 特权挂载:挂盘本身已有 root 路径在跑,host 侧不另开新的 root 挂载通道(新特权面只扩 攻击面);轮询以 chv 只读判定挂载态,零新增特权。 - attach:删 chv 调 chv-data-mount,改为轮询 mountpoint -q /home/chv/workspace && test -w(默认 60 秒/2 秒,可经 CHV_DATA_MOUNT_TIMEOUT_SECONDS / CHV_DATA_MOUNT_INTERVAL_SECONDS 注入短值);超时记 data_disk_mount_timeout 审计并抛同名 kind,不写归属标记。attach 返回成功 = 挂盘真实发生。 resume_seconds 因此真实包含挂盘等待(§2.1 口径),≤2 秒 目标不改口径、不改目标。 - detach:改走 sudo -n /usr/bin/umount /home/chv/workspace; 失败即 False,release 留置 VM 不回收(fail-closed)。 - chv-data-mount.sh:盘已找到后 mkfs/mount 失败改 exit 1 (原全部失败路径 exit 0);无盘仍 exit 0(启动期 no-op, 不挡 boot)。本脚本只由 root 跑(service/runcmd)。 - build.sh:apt 加 sudo;写 /etc/sudoers.d/chv-data-umount 窄规则(chv 仅能以 root 跑 umount /home/chv/workspace 这一条精确 argv,NOPASSWD,无 shell/其它命令/挂载权), 0440 + visudo -cf 作构建门;buildinfo/lockfile 登记 sudo 与 chv-data-umount。 特权边界:本轮唯一新增提权面就是上述一条 sudoers 规则; attach 零提权(只读轮询),挂载权仍只在 root 的 hotplug/service 路径手里。旧镜像(无 sudo/无规则)上 detach 返回 False、release 留置,须重建镜像才恢复回收。 测试:新增 10 项(attach 等三次轮询才成功、超时 loud 失败、 永不以 chv 调 chv-data-mount、detach 精确 sudo argv、 sudo 失败 False、卸载失败 release 留置、客机脚本 exit 码、 build.sh sudo/窄规则/buildinfo 登记);另修 2 处旧断言 (attach 不再调挂载脚本)与 stage4 lockfile 夹具(补 sudo dpkg 条目),并把 attach 热插段抽为 _hotplug_tenant_disk (50 行函数上限)。全量 Ran 4688,OK (skipped=15,均为 环境 skip:无 PG DSN/驱动、缺 SearXNG_URL、fallback state 文件在位)。
200 lines
8.3 KiB
Python
200 lines
8.3 KiB
Python
#!/usr/bin/env python3
|
||
"""生成 CHV 客机镜像的 BUILDINFO.json(build.sh 调用)。
|
||
|
||
从 build.sh 抽出(B-02),使其可独立单元测试。输入全部是
|
||
已落盘的构建产物与读回文件,不编造版本号:versions 来自
|
||
客机读回的 dpkg-query / pip freeze(Playwright=1 时另有
|
||
ms-playwright 缓存目录名作 chromium 标识);artifacts 的
|
||
sha256/size 是对产物文件实测的;image_sha256 即 disk.raw
|
||
的 sha256,供 generate_lockfile.py 沿用。
|
||
|
||
用法:
|
||
python3 buildinfo.py OUT_DIR TAG UBUNTU CODEX_VERSION \\
|
||
REPO_COMMIT UBUNTU_IMAGE_SHA256 PLAYWRIGHT(0|1)
|
||
[CHROMIUM_HEADLESS_SHELL_REV]
|
||
|
||
CHROMIUM_HEADLESS_SHELL_REV(可选,纯数字):钉死的
|
||
ms-playwright 缓存修订号(如 1248)。PLAYWRIGHT=1 且
|
||
给了该值时,versions.chromium 里须恰好出现
|
||
chromium_headless_shell-<rev>,否则退出 1、构建失败;
|
||
未给时只记录、不强制,BUILDINFO.chromium_pinned_rev
|
||
记 null,构建输出由 build.sh 明说“未钉死”。
|
||
"""
|
||
import hashlib
|
||
import json
|
||
import os
|
||
import re
|
||
import sys
|
||
import time
|
||
|
||
# 轻量口径(2026-11-06 用户定):Ubuntu Minimal 底座,只装
|
||
# codex + Python + SSH + cloud-init + 薄 agent + git + gt +
|
||
# 最小中文字体;已裁剪 pandoc/texlive/python-docx 等文档工具
|
||
# 与 qemu-guest-agent。chv-sync 是仓库内脚本,版本随 repo_commit。
|
||
BASE_CONTENTS = [
|
||
"git", "openssh-server", "openssh-client", "cloud-init",
|
||
"python3", "python3-pip", "curl", "ca-certificates",
|
||
"xz-utils", "fonts-wqy-microhei", "fontconfig", "codex",
|
||
"codex-code-mode-host", "codex-resources/bwrap",
|
||
"/app/host.py", "gt", "chv-sync",
|
||
# p2 第三轮:sudo(dpkg 包,版本走 dpkg freeze)+
|
||
# chv-data-umount sudoers 窄规则(版本随 repo_commit,
|
||
# 见 check/generate_lockfile.py 的 DECLARED_SOURCES)。
|
||
"sudo", "chv-data-umount",
|
||
# 对齐 Muse P2:nspawn 单元启动器 + broker + 浏览器子代理。
|
||
"systemd-container", "chv-cell", "chv-broker", "chv-browser",
|
||
# feat-chv-opt-landing(阶段 1):数据盘挂载脚本。
|
||
"chv-data-mount",
|
||
# feat-chv-opt-landing(阶段 2):客机运行时三分服务。
|
||
"chv-daemon", "chv-execd", "chv-syncd",
|
||
# feat-chv-opt-landing(阶段 2):cell veth 的 networkd
|
||
# 不管配置(须排在 80-container-ve.network 之前)。
|
||
"79-chv-cell.network",
|
||
# feat-chv-opt-landing(阶段 4):Pillow(pip 包,diff_shot
|
||
# 逐像素比对与全帧 JPEG 编码用;只加 pillow,不动其它包版本。
|
||
# check/generate lockfile 按 pip 名覆盖,不走 DECLARED_SOURCES)。
|
||
"pillow",
|
||
]
|
||
|
||
# 执行器默认内核 cmdline(config.py),构建产物须与它一起记录(B-12),
|
||
# 否则换宿主/换执行器配置时无法复现。
|
||
KERNEL_CMDLINE = "console=hvc0 root=/dev/vda1 rw"
|
||
|
||
# 浏览器装在系统路径(B-22),不是某用户的 ~/.cache:构建时
|
||
# PLAYWRIGHT_BROWSERS_PATH 指这里,agent 运行时也须设同一变量。
|
||
PLAYWRIGHT_BROWSERS_PATH = "/usr/local/share/ms-playwright"
|
||
|
||
|
||
def _read_lines(path):
|
||
try:
|
||
with open(path) as fh:
|
||
return [line.strip() for line in fh if line.strip()]
|
||
except OSError:
|
||
return []
|
||
|
||
|
||
def _sha256_size(path):
|
||
digest = hashlib.sha256()
|
||
size = 0
|
||
with open(path, "rb") as fh:
|
||
for chunk in iter(lambda: fh.read(1 << 20), b""):
|
||
digest.update(chunk)
|
||
size += len(chunk)
|
||
return digest.hexdigest(), size
|
||
|
||
|
||
def build(outdir, tag, ubuntu, codex_version, repo_commit,
|
||
ubuntu_image_sha256, playwright, chromium_pinned_rev=None,
|
||
extra_contents=None):
|
||
disk_path = os.path.join(outdir, "disk.raw")
|
||
vmlinux_path = os.path.join(outdir, "vmlinux")
|
||
disk_sha, disk_size = _sha256_size(disk_path)
|
||
vmlinux_sha, vmlinux_size = _sha256_size(vmlinux_path)
|
||
contents = list(BASE_CONTENTS)
|
||
# extra_contents:构建时才确定的附加内容(如 P1 的 vsock_notify,
|
||
# 文件存在才拷入)。调用方传什么记什么,不编默认值。
|
||
for item in extra_contents or []:
|
||
if item and item not in contents:
|
||
contents.append(item)
|
||
versions = {
|
||
"dpkg": _read_lines(
|
||
os.path.join(outdir, "guest-versions-dpkg.txt")),
|
||
"pip": _read_lines(
|
||
os.path.join(outdir, "guest-versions-pip.txt")),
|
||
"gt": _read_lines(
|
||
os.path.join(outdir, "guest-versions-gt.txt")),
|
||
}
|
||
gt_version = ""
|
||
for line in versions["gt"]:
|
||
m = re.search(r"(\d+\.\d+\.\d+)", line)
|
||
if m:
|
||
gt_version = m.group(1)
|
||
break
|
||
if playwright:
|
||
contents += ["playwright", "chromium-headless-shell"]
|
||
versions["chromium"] = _read_lines(
|
||
os.path.join(outdir, "guest-versions-chromium.txt"))
|
||
info = {
|
||
"tag": tag,
|
||
"ubuntu": ubuntu,
|
||
"codex_version": codex_version,
|
||
"repo_commit": repo_commit,
|
||
"gt_version": gt_version,
|
||
"built_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||
"contents": contents,
|
||
"versions": versions,
|
||
"ubuntu_image_sha256": ubuntu_image_sha256,
|
||
"ubuntu_image_sha256_set": True,
|
||
"kernel_cmdline": KERNEL_CMDLINE,
|
||
"playwright": bool(playwright),
|
||
"playwright_browsers_path": PLAYWRIGHT_BROWSERS_PATH
|
||
if playwright else "",
|
||
# feat-chv-closeup(M4):钉死的 Chromium 修订号(未设
|
||
# 记 null,绝不编一个默认值出来假装已钉死)。
|
||
"chromium_pinned_rev": chromium_pinned_rev or None,
|
||
"image_sha256": disk_sha,
|
||
"artifacts": {
|
||
"disk.raw": {"sha256": disk_sha, "size": disk_size},
|
||
"vmlinux": {"sha256": vmlinux_sha, "size": vmlinux_size},
|
||
},
|
||
}
|
||
path = os.path.join(outdir, "BUILDINFO.json")
|
||
with open(path, "w") as fh:
|
||
json.dump(info, fh, indent=2)
|
||
fh.write("\n")
|
||
return info
|
||
|
||
|
||
def main(argv):
|
||
if len(argv) not in (8, 9, 10):
|
||
print("用法:buildinfo.py OUT TAG UBUNTU CODEX_VERSION "
|
||
"REPO_COMMIT UBUNTU_IMAGE_SHA256 PLAYWRIGHT "
|
||
"[CHROMIUM_HEADLESS_SHELL_REV] [EXTRA_CONTENTS]", file=sys.stderr)
|
||
return 2
|
||
_, outdir, tag, ubuntu, codex_version, repo_commit, \
|
||
ubuntu_image_sha256, playwright = argv[:8]
|
||
chromium_pinned_rev = argv[8] if len(argv) >= 9 else None
|
||
# 第 10 参数:逗号分隔的附加内容(如 vsock_notify),可空。
|
||
extra_contents = None
|
||
if len(argv) == 10 and argv[9]:
|
||
extra_contents = [s for s in argv[9].split(",") if s]
|
||
if chromium_pinned_rev and not re.fullmatch(r"[0-9]+",
|
||
chromium_pinned_rev):
|
||
print("CHROMIUM_HEADLESS_SHELL_REV 须为纯数字:%s"
|
||
% chromium_pinned_rev, file=sys.stderr)
|
||
return 2
|
||
info = build(outdir, tag, ubuntu, codex_version, repo_commit,
|
||
ubuntu_image_sha256, playwright == "1",
|
||
chromium_pinned_rev, extra_contents)
|
||
if not info["versions"]["dpkg"]:
|
||
print("BUILDINFO versions.dpkg 为空", file=sys.stderr)
|
||
return 1
|
||
if not info["versions"]["pip"]:
|
||
print("BUILDINFO versions.pip 为空", file=sys.stderr)
|
||
return 1
|
||
if not info["gt_version"]:
|
||
print("BUILDINFO gt_version 为空(gt 未装好)", file=sys.stderr)
|
||
return 1
|
||
if playwright == "1" and not info["versions"].get("chromium"):
|
||
print("BUILDINFO versions.chromium 为空(浏览器未装好或"
|
||
"读回路径不对)", file=sys.stderr)
|
||
return 1
|
||
if playwright == "1" and chromium_pinned_rev:
|
||
# feat-chv-closeup(M4):钉死校验——读回的缓存目录名
|
||
# 须恰好是该修订号,playwright 升级导致目录名变了就
|
||
# 在这里失败,不静默换浏览器版本。
|
||
expected = "chromium_headless_shell-%s" % chromium_pinned_rev
|
||
if expected not in (info["versions"].get("chromium") or []):
|
||
print("Chromium 修订号与钉死值不符:读回 %s,钉死 %s"
|
||
% (info["versions"].get("chromium"), expected),
|
||
file=sys.stderr)
|
||
return 1
|
||
if info["tag"] != tag:
|
||
print("BUILDINFO tag 不一致", file=sys.stderr)
|
||
return 1
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main(sys.argv))
|