185 lines
8.1 KiB
Bash
185 lines
8.1 KiB
Bash
#!/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 ))
|