内网机/外网机各起一套栈,两台之间不开任何端口,文件只经光闸在 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 出盘。
206 lines
8.7 KiB
Bash
206 lines
8.7 KiB
Bash
#!/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 的默认口令。"
|