#!/usr/bin/env bash # # bootstrap-worker.sh —— 把一台机器拉到「可承接实例的集群节点」状态 # # 立稿 2026-09-16。起因:把 guest 从 w-47 迁到 w-106 时,节点侧缺的东西全是**手工**补的 # (装 dsh、传 runtime、改目录权限、useradd)—— 用户明确要求「该谁处理就让对应功能处理」。 # 本脚本就是那个「功能」:**幂等**、可重复执行、只做正向补齐。 # # 它覆盖四件套 + 权限 + 自检(自检项刻意包含 `dshs doctor` 漏掉的几项): # 1. dsh 主程序 —— 缺则按指定版本从 npm 装(内网/镜像优先) # 2. dsh-runtime —— jq / rg / ffmpeg / ffprobe 走本机包管理(**不要从别的机器搬**) # python 见 §3 的「运行时路径契约」 # 3. 数据根与权限 —— /var/lib/dshs 711、users 711(**漏了会让实例必崩,且不报权限错**) # 4. 旧角色残留 —— 可选(--prune-legacy) # 5. 自检 —— 四件套 + 权限 + 端口 + 数据根,任一项失败 ⇒ 退出码非 0 # # 用法: # ./bootstrap-worker.sh # 补齐 + 自检 # ./bootstrap-worker.sh --check # 只自检,不改动任何东西 # ./bootstrap-worker.sh --prune-legacy # 额外清理已知的旧控制面(停用 + 禁用,不删数据) # # 环境变量: # DSH_VERSION 要安装的 @deepseek-ai/dsh 版本(默认 0.1.5-rc.1,须与 Manager 一致) # DSHS_DATA_ROOT 数据根(默认 /var/lib/dshs) # DSHS_RUNTIME_DIR 运行时目录(默认 /usr/local/dsh-runtime) # set -uo pipefail DSH_VERSION="${DSH_VERSION:-0.1.5-rc.1}" DATA_ROOT="${DSHS_DATA_ROOT:-/var/lib/dshs}" RUNTIME_DIR="${DSHS_RUNTIME_DIR:-/usr/local/dsh-runtime}" CHECK_ONLY=0 PRUNE_LEGACY=0 FAILED=0 for arg in "$@"; do case "$arg" in --check) CHECK_ONLY=1 ;; --prune-legacy) PRUNE_LEGACY=1 ;; -h|--help) sed -n '2,25p' "$0"; exit 0 ;; *) echo "未知参数: $arg" >&2; exit 2 ;; esac done step() { printf '\n\033[1m== %s ==\033[0m\n' "$1"; } ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; } warn() { printf ' \033[33m!\033[0m %s\n' "$1"; } bad() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAILED=1; } act() { [ "$CHECK_ONLY" = 1 ] && { warn "(--check)跳过: $1"; return 1; }; return 0; } # ---------------------------------------------------------------- 1. 前置 step "1. 前置条件" [ "$(id -u)" = "0" ] && ok "以 root 运行" || bad "需要 root(useradd / systemd / chown)" command -v systemctl >/dev/null && ok "systemd 可用" || bad "缺 systemd" for c in node npm useradd chown; do command -v "$c" >/dev/null && ok "命令 $c" || bad "缺命令 $c" done command -v nft >/dev/null && ok "nft 可用" || warn "缺 nft(跨机防火墙规则会缺失)" # ---------------------------------------------------------------- 2. dsh 主程序 step "2. dsh 主程序(版本须与 Manager 一致:$DSH_VERSION)" DSH_BIN="${DSHS_DSH_BIN:-}" [ -z "$DSH_BIN" ] && DSH_BIN="$(command -v dsh || true)" if [ -n "$DSH_BIN" ] && [ -x "$DSH_BIN" ]; then have="$("$DSH_BIN" --version 2>/dev/null | head -1)" if [ "$have" = "$DSH_VERSION" ]; then ok "已安装且版本正确: $DSH_BIN ($have)" else warn "已装版本 $have ≠ 期望 $DSH_VERSION" if act "npm i -g @deepseek-ai/dsh@$DSH_VERSION"; then npm i -g "@deepseek-ai/dsh@$DSH_VERSION" >/dev/null 2>&1 && ok "已升级到 $DSH_VERSION" || bad "安装失败" fi fi else warn "未安装 dsh" if act "npm i -g @deepseek-ai/dsh@$DSH_VERSION"; then npm i -g "@deepseek-ai/dsh@$DSH_VERSION" >/dev/null 2>&1 && ok "已安装" || bad "安装失败" fi fi # 平台按裸命令 `dsh` 解析(DEFAULT_DSH_COMMAND=['dsh'])⇒ 保证 PATH 里能找到 if command -v dsh >/dev/null; then ok "PATH 可解析 dsh: $(command -v dsh)" else warn "PATH 里找不到 dsh(平台以裸命令启动实例)" for cand in /usr/bin/dsh /usr/local/bin/dsh; do [ -x "$cand" ] && { [ "$CHECK_ONLY" = 0 ] && ln -sfn "$cand" /usr/local/bin/dsh && ok "已补 /usr/local/bin/dsh → $cand"; break; } done fi # ---------------------------------------------------------------- 3. 运行时 step "3. dsh-runtime(jq / rg / ffmpeg / ffprobe)" for tool in jq rg ffmpeg ffprobe; do if command -v "$tool" >/dev/null; then ok "$tool: $(command -v "$tool")" else warn "缺 $tool" if act "包管理器安装 $tool"; then # 本机/内网源即可(实测腾讯云内网源 43 MB/s,别从别的机器搬几百 MB) if command -v dnf >/dev/null; then dnf install -y "$tool" >/dev/null 2>&1 elif command -v apt-get >/dev/null; then apt-get install -y "$tool" >/dev/null 2>&1 fi command -v "$tool" >/dev/null && ok "$tool 已安装" || bad "$tool 安装失败" fi fi done # 平台约定:实例从 $RUNTIME_DIR/bin 取工具 ⇒ 补契约链接 if [ -d "$RUNTIME_DIR/bin" ] || [ "$CHECK_ONLY" = 0 ]; then [ "$CHECK_ONLY" = 0 ] && mkdir -p "$RUNTIME_DIR/bin" for tool in jq rg ffmpeg ffprobe; do src="$(command -v "$tool" 2>/dev/null || true)" if [ -n "$src" ] && [ ! -e "$RUNTIME_DIR/bin/$tool" ]; then [ "$CHECK_ONLY" = 0 ] && ln -sfn "$src" "$RUNTIME_DIR/bin/$tool" && ok "$RUNTIME_DIR/bin/$tool → $src" fi done fi step "3b. python 运行时(路径契约)" PY="" for p in "$RUNTIME_DIR"/python-3.12.*/bin/python3.12; do [ -x "$p" ] && PY="$p" && break done if [ -n "$PY" ]; then ok "python: $PY ($("$PY" -V 2>&1))" else warn "未找到 $RUNTIME_DIR/python-3.12.*/bin/python3.12" warn "⚠️ 迁移用户的 venv 里写死了这个路径(pyvenv.cfg 的 executable)⇒ 缺失则其 Python 环境不可用" warn "正解:把与 Manager 同版本的 python-build-standalone 放到该路径(整目录可搬)" FAILED=1 fi # ---------------------------------------------------------------- 4. 数据根与权限 step "4. 数据根与权限(漏了会让实例必崩,且不报权限错)" if [ ! -d "$DATA_ROOT" ]; then if act "创建 $DATA_ROOT"; then mkdir -p "$DATA_ROOT" && ok "已创建 $DATA_ROOT"; fi else ok "$DATA_ROOT 存在" fi # 关键:users 必须 o+x(711),否则 setpriv --reuid 无法穿越 ⇒ 实例起不来 for d in "$DATA_ROOT" "$DATA_ROOT/users"; do [ -d "$d" ] || { [ "$CHECK_ONLY" = 0 ] && mkdir -p "$d"; } [ -d "$d" ] || continue mode="$(stat -c '%a' "$d")" if [ "$mode" = "711" ]; then ok "$(printf '%-28s' "$d") 权限 711" else warn "$(printf '%-28s' "$d") 权限 $mode ≠ 711" if act "chmod 711 $d"; then chmod 711 "$d" && ok "已改为 711"; fi fi done # ---------------------------------------------------------------- 5. 旧角色清理 step "5. 旧角色残留(可选)" LEGACY_UNITS="dsh-users-platform.service" found_legacy=0 for u in $LEGACY_UNITS; do if systemctl list-unit-files "$u" >/dev/null 2>&1 && systemctl list-unit-files | grep -q "^$u"; then found_legacy=1 if [ "$PRUNE_LEGACY" = 1 ]; then if act "停用 $u"; then systemctl stop "$u" >/dev/null 2>&1; systemctl disable "$u" >/dev/null 2>&1 && ok "已停用并禁用 $u(数据未删)" fi else warn "$u 仍在(用 --prune-legacy 停用;本节点若已切角色为纯 worker 才该执行)" fi fi done [ "$found_legacy" = 0 ] && ok "未发现已知旧角色单元" # ---------------------------------------------------------------- 6. 自检汇总 step "6. 自检汇总" rc=0 { command -v dsh >/dev/null && [ -x "$(command -v dsh)" ]; } || { bad "dsh 不可执行"; rc=1; } [ -n "$PY" ] || { bad "python 运行时缺失"; rc=1; } for tool in jq rg ffmpeg ffprobe; do command -v "$tool" >/dev/null || { bad "缺 $tool"; rc=1; }; done [ "$(stat -c '%a' "$DATA_ROOT/users" 2>/dev/null)" = "711" ] || { bad "$DATA_ROOT/users 权限非 711"; rc=1; } command -v setpriv >/dev/null || { bad "缺 setpriv"; rc=1; } command -v bwrap >/dev/null || warn "缺 bwrap(隔离靠它;补齐前不要承接实例)" if [ "$rc" = 0 ] && [ "$FAILED" = 0 ]; then printf '\n\033[32m节点自检通过:可以承接实例。\033[0m\n' else printf '\n\033[31m节点未就绪(上面 ✗ 项必须解决)。\033[0m\n' fi exit $(( rc || FAILED ))