Files
senmesh-zc/deploy/offline/scripts/install.sh
KodouWZF 4eb67e246e feat(交付): 客户侧两台服务器离线部署包
内网机/外网机各起一套栈,两台之间不开任何端口,文件只经光闸在
gate/out 与对端 gate/in 之间单向摆渡。一份 compose + 一份 config +
一份 nginx 模板,全部差异收在 .env 里。

- docker-compose.yml:5 服务只发布前端端口;app 容器 IP 锁死并由
  compose 注入 -Dcode.snowflake.workid.<IP>=<WORK_ID>,绕开兜底 99
  超雪花 5 位上限导致启动即崩的问题
- .env.iw / .env.ow:出厂配置与随机口令已填好,仅 MESHSAFE_SECRET
  故意留占位(以 MeshSafe 侧白名单为准);按 maintainer 决定原样入库
- install.sh iw|ow:按角色建 .env -> 自检 -> sha256 校验 tar -> 架构
  核对 -> docker load 后校验 tag -> 建目录/属主/SELinux 标签 -> 起栈
  并逐个等健康。修掉 pipefail 下 `grep | head -1` 静默中断、架构字段
  误读 manifest.json(实际在 Config blob)、die() 未定义先被调用
- export-images.sh:就地刷新 images/*.tar、sql、.env 的 tag 与
  SHA256SUMS,再打 dist 交付盘;tar 与 SHA256SUMS 由 .gitignore 排除
- sql/01+02:从 backend/db 的 PG 全量与初始数据覆盖拷贝。三个
  upgrade-*.sql 的 PG 段(x_upload_config、x_file_security)已在
  init-table-postgres-full.sql 内,空库跑 01->02 即完整现状
- smoke_test.py:18 条断言的现场验收,凭证全部走环境变量
- 部署说明.md:拓扑、交付清单、出厂口令表、iw/ow 差异、验收清单、
  13 条已知坑

镜像 tar 不进 git,装机包由 export-images.sh 出盘。
2026-11-02 20:40:16 +08:00

206 lines
8.7 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
# =============================================================================
# s-file 客户侧一键安装(在每台服务器上各执行一次)
#
# cd /opt/sfile && ./scripts/install.sh iw # 内网机
# cd /opt/sfile && ./scripts/install.sh ow # 外网机
#
# 首次执行会按角色把交付包里的 .env.iw / .env.ow 拷成 .env;之后改 .env 即可,
# 想回到交付默认值就删掉 .env 重跑。
#
# 做五件事:按角色选配置 -> 环境自检 -> 载入镜像 tar -> 建数据目录 -> 起栈并等到健康
# 幂等:重复执行只重建有差异的容器,不动已初始化好的数据库
# =============================================================================
set -euo pipefail
HERE="$(cd "$(dirname "$0")/.." && pwd)"
cd "$HERE"
ROLE_ARG="${1:-}"
envget() { grep -E "^[[:space:]]*$1=" .env | tail -1 | cut -d= -f2- | tr -d '"' | sed 's/[[:space:]]*$//'; }
step() { printf '\n\033[1;36m==> %s\033[0m\n' "$1"; }
die() { printf '\033[1;31m!!! %s\033[0m\n' "$1" >&2; exit 1; }
case "$ROLE_ARG" in
""|iw|ow) ;;
*) die "未知角色「$ROLE_ARG」,只支持 iw(内网机)或 ow(外网机)" ;;
esac
# ---------- 0. 配置就位 ----------
if [ ! -f .env ]; then
case "$ROLE_ARG" in
iw|ow)
[ -f ".env.$ROLE_ARG" ] || die "缺少 .env.$ROLE_ARG(交付包不完整)"
step "按角色初始化配置:cp .env.$ROLE_ARG .env"
cp ".env.$ROLE_ARG" .env
;;
"") die "用法:./scripts/install.sh iw|ow(iw=内网机,ow=外网机)" ;;
esac
elif [ -n "$ROLE_ARG" ]; then
# 已有 .env 时不让角色参数悄悄覆盖现场改动
CUR_ROLE="$(envget SFILE_ROLE)"
[ "$CUR_ROLE" = "$ROLE_ARG" ] \
|| die ".env 已存在且角色是 $CUR_ROLE,与参数 $ROLE_ARG 不符;确认要换角色请先 rm .env"
fi
# ---------- 0. 前置自检 ----------
step "检查 docker 与 compose"
command -v docker >/dev/null 2>&1 || die "未安装 docker,请先装 Docker Engine(信创系统用厂商源,版本号 >= 20.10)"
docker info >/dev/null 2>&1 || die "docker 守护进程不可用,检查服务是否启动、当前用户是否在 docker 组"
if docker compose version >/dev/null 2>&1; then
DC="docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
DC="docker-compose"
else
die "缺少 compose:安装 docker compose v2 插件,或用 docker-compose 命令"
fi
echo " 使用:$DC"
DOCKER_VER="$(docker version --format '{{.Server.Version}}')"
echo " docker server:$DOCKER_VER"
[ -f .env ] || die "缺少 .env —— 先执行:cp .env.example .env && vi .env"
if grep -q "CHANGE_ME" .env; then
# MeshSafe 的 secret 只有真的启用检测时才必须填,否则客户没接 MeshSafe 也装不了机
if [ "$(envget MESHSAFE_ENABLED)" = "true" ] && grep -qE "^MESHSAFE_SECRET=.*CHANGE_ME" .env; then
die "启用了 MeshSafe 但 MESHSAFE_SECRET 还是占位值:按 MeshSafe 交付单改成白名单里的 secret,或把 MESHSAFE_ENABLED 改成 false"
fi
rest="$(grep -n CHANGE_ME .env | grep -v '^.*MESHSAFE_SECRET=' | cut -d: -f2 | tr '\n' ' ')"
[ -z "$rest" ] || die ".env 里还有 CHANGE_ME 未替换:$rest"
fi
step "检查交付物"
[ -d images ] || die "缺少 images/ 目录(镜像 tar 应与本脚本同级目录的 images/ 下)"
tar_count=$(find images -maxdepth 1 -name '*.tar' | wc -l)
[ "$tar_count" -eq 5 ] || die "images/ 下应有 5 个 .tar(app/web/pg/redis/mq),实际 $tar_count 个:$(ls images)"
for f in sql/01-init-table.sql sql/02-init-data.sql; do
[ -s "$f" ] || die "缺少 $f,参见 sql/README.md"
done
echo " 镜像 tar:5 个,SQL 脚本就位"
# ---------- 1. 校验交付盘 + 镜像架构 ----------
# 650M 的 tar 经 U 盘/内网 scp 拷坏是最常见的现场故障,坏包 load 到一半才报错更难查
if [ -f images/SHA256SUMS.txt ] && [ "${SKIP_CHECKSUM:-0}" != "1" ]; then
step "校验镜像 tar 完整性(sha256,约半分钟)"
( cd images && sha256sum -c SHA256SUMS.txt ) \
|| die "有 tar 与 SHA256SUMS.txt 不一致:交付盘拷坏了,重新拷一份;确认无碍可 SKIP_CHECKSUM=1 跳过"
fi
step "核对镜像架构"
HOST_ARCH="$(uname -m)"
case "$HOST_ARCH" in
x86_64) EXPECT="amd64" ;;
aarch64|arm64) EXPECT="arm64" ;;
*) EXPECT="unknown" ;;
esac
echo " 本机:$HOST_ARCH (期望镜像架构 ${EXPECT})"
# 架构在镜像的 config blob 里,不在 manifest.json —— manifest.json 只给 Config 的路径
tar_arch() {
local t="$1" cfg
cfg="$(tar -xOf "$t" manifest.json 2>/dev/null | tr -d '\n' \
| { grep -oE '"Config":"[^"]+"' || true; } | awk -F'"' 'NR==1{print $4}')"
[ -n "$cfg" ] || { echo ""; return 0; }
tar -xOf "$t" "$cfg" 2>/dev/null | tr -d '\n' \
| { grep -oE '"architecture":"[a-z0-9-]+"' || true; } | awk -F'"' 'NR==1{print $4}'
}
if [ "$EXPECT" != "unknown" ]; then
# app 和 pg 各查一个:一个是自己构建的,一个是基础镜像,架构错了必是出盘时选错平台
for t in images/sfile-app.tar images/postgres.tar; do
[ -f "$t" ] || continue
# 不能用 `| head -1`:pipefail 下 head 提前关管道会让上游 grep 收到 SIGPIPE(141),脚本静默中断
IMG_ARCH="$(tar_arch "$t")"
if [ -n "${IMG_ARCH:-}" ] && [ "$IMG_ARCH" != "$EXPECT" ]; then
die "$(basename "$t") 是 $IMG_ARCH,本机是 $EXPECT —— 别硬装,回来重出对应架构的包"
fi
printf ' %-18s %s\n' "$(basename "$t")" "${IMG_ARCH:-未识别,继续}"
done
fi
# ---------- 2. 载入镜像 ----------
step "docker load 载入离线镜像"
for t in images/*.tar; do
echo " <- $t"
docker load -i "$t"
done
# compose 用的是 .env 里的 tag,缺一个就会去联网拉,离线机上直接卡死
for img in "$(envget APP_IMAGE)" "$(envget WEB_IMAGE)" "$(envget PG_IMAGE)" \
"$(envget REDIS_IMAGE)" "$(envget MQ_IMAGE)"; do
docker image inspect "$img" >/dev/null 2>&1 \
|| die "载入后仍看不到镜像 $img —— tar 与 .env 的 tag 不一致,别继续,回来看 images/ 和 .env"
echo " ✓ $img"
done
# ---------- 3. 目录 ----------
step "创建数据与摆渡目录"
DATA_ROOT="$(envget SFILE_DATA_ROOT)"
GATE_SEND="$(envget GATE_SEND_DIR)"
GATE_RECV="$(envget GATE_RECV_DIR)"
[ -n "$DATA_ROOT" ] || die ".env 里 SFILE_DATA_ROOT 为空"
mkdir -p "$DATA_ROOT"/{upload,logs,pgdata,redis,mq}
mkdir -p "$GATE_SEND" "$GATE_RECV"
# 数据盘挂载点没起来时直接建目录会落到根分区,先确认一下
if ! mountpoint -q "$DATA_ROOT" 2>/dev/null; then
printf ' \033[1;33m提示:%s 不是独立挂载点,确认容量够(上传+库+日志)\033[0m\n' "$DATA_ROOT"
fi
# pgdata 提前给到容器内 postgres 的 uid=999,避免镜像入口脚本 chown 失败;
# redis/mq 的官方入口脚本自己会改属主,这里只保证目录存在且可写
chown -R 999:999 "$DATA_ROOT/pgdata" 2>/dev/null || true
chmod -R 750 "$DATA_ROOT"
# SELinux(麒麟/统信多为 enforcing):无 :z 标记的绑定挂载需要放开标签,否则库起不来
if command -v chcon >/dev/null 2>&1; then
chcon -Rt svirt_sandbox_file_t "$DATA_ROOT" "$GATE_SEND" "$GATE_RECV" 2>/dev/null \
|| echo " chcon 失败(未启用 SELinux 可忽略)"
fi
echo " $DATA_ROOT / $GATE_SEND / $GATE_RECV 就位"
# ---------- 4. 起栈 ----------
step "启动服务(首次建库约 1-3 分钟)"
$DC up -d
step "等待健康检查"
ROLE="$(envget SFILE_ROLE)"
for svc in pg redis mq app web; do
name="sfile-${ROLE}-${svc}"
printf ' %-24s ' "$name"
for _ in $(seq 1 60); do
st="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$name" 2>/dev/null || echo missing)"
case "$st" in
healthy|running) printf '\033[32m%s\033[0m\n' "$st"; break ;;
missing) sleep 2 ;;
*) sleep 5 ;;
esac
done
[ "${st:-}" = "healthy" ] || [ "${st:-}" = "running" ] || {
printf '\033[31m超时(最后状态:%s)\033[0m\n' "${st:-unknown}"
$DC ps
echo " 看日志:docker logs --tail 200 ${name}"
exit 1
}
done
# ---------- 5. 出口信息 ----------
IP="$(hostname -I 2>/dev/null | tr ' ' '\n' | { grep -E '^[0-9]+\.' || true; } | awk 'NR==1{print}')"
PORT="$(envget WEB_PORT)"
step "完成"
cat <<EOF
访问地址: http://${IP:-<本机IP>}:${PORT:-80}
角色: ${ROLE}(传输方向 location=$(envget SFILE_TRANSFER_LOCATION),站点 $(envget SFILE_SITE_ROOT))
摆渡: 发出 $(envget GATE_SEND_DIR) / 收到 $(envget GATE_RECV_DIR)
MeshSafe: $(envget MESHSAFE_ENABLED) -> $(envget MESHSAFE_BASE_URL)
常用命令:
$DC ps 看状态
docker logs -f sfile-${ROLE}-app 看后端日志
$DC restart app 改完 .env/config 后重启后端
EOF
echo " 首次登录后立刻改掉 admin 的默认口令。"