Files
dsh_shenxian/scripts/bootstrap-worker.sh
T

185 lines
8.1 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 <uid> 无法穿越 ⇒ 实例起不来
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 ))