#!/bin/bash # verify_profile.sh —— 防漂移核验:运行中容器 vs 仓库 profile 声明 # 在目标服务器上运行(需 docker 读权限,无需 GPU)。用法: # bash verify_profile.sh deploy/profiles/pro6000/glm53_nvfp4_pro6000_sglang_tp2pp4.env # bash verify_profile.sh [container_name] # container_name 缺省取 profile 的 CONTAINER_NAME # 核验项:①容器在跑 ②镜像 registry digest ③launch 参数(token 级比对)④端口监听。 # PD 链角色的参数在挂载的 launch 脚本内(docker Args 只有 bash /smoke_launch.sh), # 第③项自动跳过并提示改用库内脚本 md5 对比(scripts/ 目录各脚本头注释有 md5)。 set -uo pipefail PROFILE="$1" [ -r "$PROFILE" ] || { echo "FATAL: 无法读取 profile: $PROFILE"; exit 2; } # profile 是声明式清单:BOOTSTRAP 行引用 ${LAUNCH_ARGS}(定义在后),set -u 下直接 # source 会炸;临时关 -u。source 完成后 LAUNCH_ARGS 已是全部变量展开后的实参串。 set +u # shellcheck disable=SC1090 source "$PROFILE" set -u CONT="${2:-${CONTAINER_NAME:-}}" fail=0 section() { printf '\n== %s ==\n' "$1"; } section "容器状态" if [ -z "$CONT" ]; then echo "FATAL: profile 未定义 CONTAINER_NAME 且未显式传入"; exit 2; fi if docker ps --format '{{.Names}}' | grep -qx "$CONT"; then echo "OK $CONT 在跑" else echo "FAIL $CONT 未在运行"; docker ps -a --filter "name=$CONT" --format '{{.Names}} {{.Status}}' | head -3 exit 1 fi section "镜像 digest" if [ -n "${DOCKER_IMAGE_DIGEST:-}" ]; then # RepoDigests 在 image 对象上:container 先取 image ID,再 image inspect img_id=$(docker inspect --format '{{.Image}}' "$CONT" 2>/dev/null) repo_digests=$(docker image inspect --format '{{join .RepoDigests "\n"}}' "$img_id" 2>/dev/null) digest_hex="${DOCKER_IMAGE_DIGEST#sha256:}" if echo "$repo_digests" | grep -q "$digest_hex"; then echo "OK registry digest 一致: ${DOCKER_IMAGE_DIGEST}" elif [ "$img_id" = "sha256:${digest_hex}" ]; then echo "OK image ID 一致: ${DOCKER_IMAGE_DIGEST}" else echo "FAIL digest 不一致" echo " profile: ${DOCKER_IMAGE_DIGEST}" echo " 实际 image ID: ${img_id}" echo " 实际 RepoDigests: $(echo "$repo_digests" | head -2 | tr '\n' ' ')" fail=1 fi else echo "SKIP profile 未定义 DOCKER_IMAGE_DIGEST" fi section "启动参数" args=$(docker inspect --format '{{join .Args " "}}' "$CONT" 2>/dev/null) if echo "$args" | grep -q launch_server; then actual=$(echo "$args" | sed 's/.*launch_server //') expected="${LAUNCH_ARGS:-}" tr ' ' '\n' <<<"$expected" | sed '/^$/d' | sort > /tmp/vp_expected.$$ tr ' ' '\n' <<<"$actual" | sed '/^$/d' | sort > /tmp/vp_actual.$$ if diff -q /tmp/vp_expected.$$ /tmp/vp_actual.$$ >/dev/null; then echo "OK 参数一致(token 比对,共 $(wc -l < /tmp/vp_expected.$$) 项)" else echo "FAIL 参数有漂移(< profile 声明 / > 容器实际):" diff /tmp/vp_expected.$$ /tmp/vp_actual.$$ | sed 's/^/ /' fail=1 fi rm -f /tmp/vp_expected.$$ /tmp/vp_actual.$$ else echo "SKIP docker Args 为 '$args' —— 参数在挂载的 launch 脚本内" echo " 改用库内脚本 md5 对比:experiments/.../scripts/ 各脚本头注释" fi section "端口 ${PORT:-?}" if [ -n "${PORT:-}" ]; then if ss -tln | grep -q ":${PORT} "; then echo "OK :${PORT} 在监听" else echo "FAIL :${PORT} 未监听"; fail=1 fi if [ -n "${HEALTH_PATH:-}" ]; then code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:${PORT}${HEALTH_PATH}" 2>/dev/null) [ "$code" = "200" ] && echo "OK health ${HEALTH_PATH} -> 200" || { echo "FAIL health ${HEALTH_PATH} -> ${code}"; fail=1; } fi else echo "SKIP profile 未定义 PORT" fi echo [ $fail -eq 0 ] && echo "VERDICT: PASS" || echo "VERDICT: DRIFT DETECTED" exit $fail