diff --git a/chv-guest/build.sh b/chv-guest/build.sh index df4a83c..6dbc7ef 100755 --- a/chv-guest/build.sh +++ b/chv-guest/build.sh @@ -172,6 +172,13 @@ if [ ! -f "$SCRIPT_DIR/chv-sync.sh" ]; then echo "缺少 chv-sync.sh(Gitea 双向同步脚本):$SCRIPT_DIR" >&2 exit 1 fi +# feat-chv-opt-landing(阶段 1):数据盘挂载脚本缺失即失败 +#(与 chv-sync.sh 同款门:构建产物须含它,否则工作区落回 +# 系统盘、"独立持久" 只剩文档声称)。 +if [ ! -f "$SCRIPT_DIR/chv-data-mount.sh" ]; then + echo "缺少 chv-data-mount.sh(数据盘挂载脚本):$SCRIPT_DIR" >&2 + exit 1 +fi # 对齐 Muse P2:单元启动器 / broker / 浏览器子代理三脚本缺一即失败。 for p2_script in chv-cell.py chv-broker.py chv-browser.py; do if [ ! -f "$SCRIPT_DIR/$p2_script" ]; then @@ -281,11 +288,13 @@ VC_ARGS=(-a "$OUT/disk.raw" --run-command 'mkdir -p /app/work && chmod 1777 /app/work && test -d /app/work && [ "$(stat -c %a /app/work)" = "1777" ]' --copy-in "$SCRIPT_DIR/chv-sync.sh:/usr/local/bin/" --run-command 'mv /usr/local/bin/chv-sync.sh /usr/local/bin/chv-sync && chmod +x /usr/local/bin/chv-sync' + --copy-in "$SCRIPT_DIR/chv-data-mount.sh:/usr/local/bin/" + --run-command 'mv /usr/local/bin/chv-data-mount.sh /usr/local/bin/chv-data-mount && chmod +x /usr/local/bin/chv-data-mount' --copy-in "$SCRIPT_DIR/chv-cell.py:/usr/local/bin/" --copy-in "$SCRIPT_DIR/chv-broker.py:/usr/local/bin/" --copy-in "$SCRIPT_DIR/chv-browser.py:/usr/local/bin/" --run-command 'mv /usr/local/bin/chv-cell.py /usr/local/bin/chv-cell && mv /usr/local/bin/chv-broker.py /usr/local/bin/chv-broker && mv /usr/local/bin/chv-browser.py /usr/local/bin/chv-browser && chmod +x /usr/local/bin/chv-cell /usr/local/bin/chv-broker /usr/local/bin/chv-browser' - --run-command 'printf "runcmd:\n - systemctl restart ssh || service ssh restart || true\n - test -f /etc/chv/guest-env && chown chv:chv /etc/chv/guest-env || true\n - /usr/local/bin/chv-sync boot >>/var/log/chv-sync.log 2>&1 || true\n" > /etc/cloud/cloud.cfg.d/99-chv-sync.cfg' + --run-command 'printf "runcmd:\n - systemctl restart ssh || service ssh restart || true\n - test -f /etc/chv/guest-env && chown chv:chv /etc/chv/guest-env || true\n - /usr/local/bin/chv-data-mount >>/var/log/chv-data-mount.log 2>&1 || true\n - /usr/local/bin/chv-sync boot >>/var/log/chv-sync.log 2>&1 || true\n" > /etc/cloud/cloud.cfg.d/99-chv-sync.cfg' --run-command 'git config --system user.name chv && git config --system user.email chv@localhost && git config --system credential.helper store && git config --system init.defaultBranch main' --run-command "$GT_INSTALL_CMD" --run-command '/usr/local/bin/gt version > /app/guest-versions-gt.txt' diff --git a/chv-guest/buildinfo.py b/chv-guest/buildinfo.py index f3af51d..c56afa0 100644 --- a/chv-guest/buildinfo.py +++ b/chv-guest/buildinfo.py @@ -39,6 +39,8 @@ BASE_CONTENTS = [ "/app/host.py", "gt", "chv-sync", # 对齐 Muse P2:nspawn 单元启动器 + broker + 浏览器子代理。 "systemd-container", "chv-cell", "chv-broker", "chv-browser", + # feat-chv-opt-landing(阶段 1):数据盘挂载脚本。 + "chv-data-mount", ] # 执行器默认内核 cmdline(config.py),构建产物须与它一起记录(B-12), diff --git a/chv-guest/check_lockfile.py b/chv-guest/check_lockfile.py index 98bd1b8..297cbaf 100644 --- a/chv-guest/check_lockfile.py +++ b/chv-guest/check_lockfile.py @@ -30,6 +30,9 @@ DECLARED_SOURCES = { "chv-cell": "repo_commit", "chv-broker": "repo_commit", "chv-browser": "repo_commit", + # feat-chv-opt-landing(阶段 1):数据盘挂载脚本,版本随 + # repo_commit(与 chv-sync 同款)。 + "chv-data-mount": "repo_commit", } diff --git a/chv-guest/chv-data-mount.sh b/chv-guest/chv-data-mount.sh new file mode 100755 index 0000000..19c1936 --- /dev/null +++ b/chv-guest/chv-data-mount.sh @@ -0,0 +1,48 @@ +#!/bin/sh +# chv-data-mount:把数据盘(LABEL=CHVDATA)挂到 /home/chv/workspace。 +# +# 由镜像 99-chv-sync.cfg runcmd 在 chv-sync boot 之前调(final 阶段, +# 用户 chv 已存在,可 chown)。幂等:已挂载即返回;已有 CHVDATA +# 标签的盘即直接挂;无数据盘(copy 模式旧 VM 没有第三块盘)即 +# 静默退出 0,不报错、不挡启动。 +# +# 找盘不依赖 /dev/vd? 固定名:先按 LABEL 找;找不到则在所有整盘里 +# 找一块无文件系统、不是根盘的盘——seed ISO 有 iso9660,不会被选 +# 中;根盘本身无 fs,但按持有 / 分区的盘排除;分区(/dev/vda1 等) +# 不参与(glob 只取整盘名)。 +# +# 阶段 1(feat-chv-opt-landing):工作区与浏览器 profile 的载体从 +# 系统盘 overlay 里剥离出来,独立持久;快照只登记系统盘 overlay, +# 数据盘不进快照(见 cloud/chv/disk.py)。 +set -u +MOUNT=/home/chv/workspace +LABEL=CHVDATA + +log() { echo "chv-data-mount: $*"; } + +mountpoint -q "$MOUNT" 2>/dev/null && { log "already mounted"; exit 0; } + +DEV=$(blkid -L "$LABEL" 2>/dev/null || true) +if [ -z "$DEV" ]; then + ROOT_SRC=$(findmnt -n -o SOURCE / 2>/dev/null || echo "") + ROOT_DISK=$(printf '%s' "$ROOT_SRC" | sed -e 's/p[0-9]*$//' -e 's/[0-9]*$//') + for d in /dev/vd[a-z] /dev/sd[a-z] /dev/xvd[a-z]; do + [ -b "$d" ] || continue + [ "$d" = "$ROOT_DISK" ] && continue + TYPE=$(blkid -o value -s TYPE "$d" 2>/dev/null || echo none) + [ "$TYPE" = "none" ] || continue + DEV=$d + break + done +fi + +[ -n "$DEV" ] || { log "no data disk found, skip"; exit 0; } +if [ "$(blkid -o value -s LABEL "$DEV" 2>/dev/null || echo "")" != "$LABEL" ]; then + log "formatting $DEV as $LABEL" + mkfs.ext4 -L "$LABEL" "$DEV" || { log "mkfs failed on $DEV"; exit 0; } +fi +mkdir -p "$MOUNT" +mount "LABEL=$LABEL" "$MOUNT" || { log "mount failed"; exit 0; } +chown chv:chv "$MOUNT" || true +log "mounted $DEV -> $MOUNT" +exit 0 diff --git a/chv-guest/generate_lockfile.py b/chv-guest/generate_lockfile.py index 8876799..c72ee64 100644 --- a/chv-guest/generate_lockfile.py +++ b/chv-guest/generate_lockfile.py @@ -51,6 +51,9 @@ DECLARED_SOURCES = { "chv-cell": "repo_commit", "chv-broker": "repo_commit", "chv-browser": "repo_commit", + # feat-chv-opt-landing(阶段 1):数据盘挂载脚本,版本随 + # repo_commit(与 chv-sync 同款)。 + "chv-data-mount": "repo_commit", } diff --git a/cloud/tests/test_chv_muse_p2.py b/cloud/tests/test_chv_muse_p2.py index 2ca4069..7c7559e 100644 --- a/cloud/tests/test_chv_muse_p2.py +++ b/cloud/tests/test_chv_muse_p2.py @@ -568,7 +568,8 @@ class BuildDryRunTest(unittest.TestCase): os.makedirs(os.path.join(bindir, "codex-resources")) for name in ("build.sh", "buildinfo.py", "check_lockfile.py", "generate_lockfile.py", "chv-sync.sh", - "chv-cell.py", "chv-broker.py", "chv-browser.py"): + "chv-cell.py", "chv-broker.py", "chv-browser.py", + "chv-data-mount.sh"): shutil.copy2(ROOT / "chv-guest" / name, os.path.join(guest, name)) shutil.copy2(ROOT / "host.py", os.path.join(repo, "host.py")) diff --git a/cloud/tests/test_chv_opt_stage1.py b/cloud/tests/test_chv_opt_stage1.py index dc3418d..a0ba225 100644 --- a/cloud/tests/test_chv_opt_stage1.py +++ b/cloud/tests/test_chv_opt_stage1.py @@ -653,5 +653,55 @@ class PersistentPoolTest(ChvOpt1Case): self.assertEqual(len(self.launches), 1) +# -- 数据盘挂载脚本与构建接线 ---------------------------------------------- +class DataMountScriptTest(unittest.TestCase): + def _repo(self, *parts): + return os.path.join( + Path(__file__).resolve().parents[2], *parts) + + def test_mount_script_present_and_safe(self): + path = self._repo("chv-guest", "chv-data-mount.sh") + with open(path) as fh: + text = fh.read() + self.assertIn("LABEL=CHVDATA", text) + self.assertIn("/home/chv/workspace", text) + self.assertIn("mkfs.ext4", text) + # 无数据盘即静默退出 0,不挡启动;绝不含任何秘密值。 + self.assertIn("no data disk found, skip", text) + for secret in ("GITEA_TOKEN", "sk-", "password"): + self.assertNotIn(secret, text) + + def test_build_sh_wires_mount_script(self): + with open(self._repo("chv-guest", "build.sh")) as fh: + text = fh.read() + self.assertIn("chv-data-mount.sh:/usr/local/bin/", text) + self.assertIn("chv-data-mount >>/var/log/chv-data-mount.log", + text) + # 挂载在 chv-sync boot 之前(工作区须先挂好再 clone): + # 以 99-chv-sync.cfg 那一行为准(文件头注释里也出现 + # chv-sync boot 字样,不能全文 index 比对)。 + cfg_line = next( + line for line in text.splitlines() + if "99-chv-sync.cfg" in line and "runcmd" in line) + self.assertLess(cfg_line.index("chv-data-mount >>"), + cfg_line.index("chv-sync boot")) + + def test_lockfile_declares_mount_script(self): + import importlib.util + guest = self._repo("chv-guest") + spec = importlib.util.spec_from_file_location( + "stage1_buildinfo", os.path.join(guest, "buildinfo.py")) + buildinfo = importlib.util.module_from_spec(spec) + spec.loader.exec_module(buildinfo) + self.assertIn("chv-data-mount", buildinfo.BASE_CONTENTS) + for name in ("check_lockfile.py", "generate_lockfile.py"): + spec = importlib.util.spec_from_file_location( + "stage1_" + name[:-3], os.path.join(guest, name)) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + self.assertEqual(mod.DECLARED_SOURCES["chv-data-mount"], + "repo_commit") + + if __name__ == "__main__": unittest.main()