Files
claude_dev 54f9ab4c4f docs(deploy): VPS生产runbook+三机环境矩阵+NAS ops脚本+修rsync危险命令
vps-production-runbook(VPS运维一站式,已验证状态+拓扑+发布流水线+人类闸口+排障); env-version-matrix(三机Python/deps矩阵+Lock建议); scripts/ops(NAS bridge探针-容器无curl-+VPS→NAS备份脚本); nas-deploy-plan§3(修rsync --exclude语法,原会删万级staging parquet+破坏entrypoint启动).
2026-07-15 07:12:46 +08:00

189 lines
6.4 KiB
Bash
Executable File
Raw Permalink 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.
#!/bin/bash
# nas_backup_vps.sh — 从生产 VPS 拉取状态到 NAS 做每日快照备份(模板)。
#
# 方向(铁律):生产 VPS → NAS 单向;**绝不反向写**(NAS 是测试/备份角色,
# 不向生产回灌数据,防止测试库污染生产库)。
#
# 这是**模板**:源侧(VPS)的具体路径随生产拓扑填(--paths),host 别名随
# ~/.ssh/config 配。脚本不硬编码任何 host / 路径 / 密钥。
#
# 策略:
# - rsync over ssh,幂等(同一快照目录重复跑结果一致)
# - 每日快照子目录 $DEST/YYYYMMDD/,用 --link-dest 指向上一份做硬链接去重
# (未变文件零额外占盘,7 份快照近乎 1 份体积)
# - 保留最近 N 份(--keep,默认 7),旧的自动删
# - --dry-run 只看不动;preflight 检查源 host 可达
#
# 用法:
# nas_backup_vps.sh \
# --host sanguo-vps \
# --paths /var/lib/sanguo/data,/opt/sanguo/config \
# --dest /volume1/stock/backup/sanguo_vps \
# --keep 7
# nas_backup_vps.sh ... --dry-run # 只打印会做什么
#
# 前置(生产拓扑就绪后做一次):
# 1) 在 NAS 的 ~/.ssh/config 加生产 host 别名 + key(免密)
# (现在别名可能还没有——preflight 会清晰报错并给出提示)
# 2) 确保 NAS 上 $DEST 根目录已创建并可写
set -euo pipefail
# ----------------------------------------------------------------------------
# 参数解析
# ----------------------------------------------------------------------------
HOST=""
PATHS=""
DEST=""
KEEP=7
DRY_RUN=0
usage() {
cat <<'EOF'
用法: nas_backup_vps.sh --host <ssh-alias> --paths <a,b,c> --dest <dir> [--keep N] [--dry-run]
必填:
--host <alias> 生产源 host 的 ssh 别名(~/.ssh/config
--paths <a,b,...> 生产侧要备份的目录列表(逗号分隔,绝对路径)
--dest <dir> NAS 上备份根目录(快照建在其下 YYYYMMDD/)
可选:
--keep N 保留最近 N 份快照(默认 7)
--dry-run 只打印会做什么,不实际传输/删除
-h, --help 显示本帮助
EOF
}
while [ $# -gt 0 ]; do
case "$1" in
--host) HOST="$2"; shift 2 ;;
--paths) PATHS="$2"; shift 2 ;;
--dest) DEST="$2"; shift 2 ;;
--keep) KEEP="$2"; shift 2 ;;
--dry-run) DRY_RUN=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "未知参数: $1" >&2; usage >&2; exit 2 ;;
esac
done
# 必填校验
MISSING=""
[ -z "$HOST" ] && MISSING="$MISSING --host"
[ -z "$PATHS" ] && MISSING="$MISSING --paths"
[ -z "$DEST" ] && MISSING="$MISSING --dest"
if [ -n "$MISSING" ]; then
echo "[FAIL] 缺少必填参数:$MISSING" >&2
usage >&2
exit 2
fi
# KEEP 数值校验
if ! [[ "$KEEP" =~ ^[0-9]+$ ]] || [ "$KEEP" -lt 1 ]; then
echo "[FAIL] --keep 必须是 >=1 的整数,得到: $KEEP" >&2
exit 2
fi
RSYNC_FLAGS=(-a --delete --info=stats1)
LINK_DEST_OPT=""
if [ "$DRY_RUN" -eq 1 ]; then
RSYNC_FLAGS+=(-n)
echo "[MODE] DRY-RUN(不实际传输/删除)"
fi
TODAY="$(date +%Y%m%d)"
SNAP="$DEST/$TODAY"
PREV="$(ls -1 "$DEST" 2>/dev/null | grep -E '^[0-9]{8}$' | sort | tail -1 || true)"
echo "=== VPS→NAS 备份 ==="
echo " host = $HOST"
echo " paths = $PATHS"
echo " dest = $DEST"
echo " snap = $SNAP"
echo " keep = $KEEP"
echo " prev = ${PREV:-(无,首份快照)}"
echo " dryrun = $DRY_RUN"
echo
# ----------------------------------------------------------------------------
# Preflight:源 host 可达性
# host 别名可能还不存在(生产拓扑未就绪)——给清晰提示而非晦涩的 ssh 报错。
# ----------------------------------------------------------------------------
echo "[preflight] 检查源 host 可达: $HOST"
if ! ssh -o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new \
"$HOST" 'echo OK' >/dev/null 2>&1; then
cat >&2 <<EOF
[FAIL] 源 host 不可达: $HOST
排查:
1) ~/.ssh/config 是否有该 host 别名?若无,加一段(占位示例):
Host $HOST
HostName <生产 VPS IP 或 域名>
User <ssh 用户>
IdentityFile ~/.ssh/<生产 key>
2) key 是否免密?本脚本用 BatchMode(绝不交互输密码),须 key 认证。
3) 网络/防火墙是否放通 ssh 端口。
4) 先手动跑一次确认: ssh $HOST echo OK
EOF
exit 1
fi
echo "[preflight] OK: $HOST 可达"
echo
# ----------------------------------------------------------------------------
# 备份根目录
# ----------------------------------------------------------------------------
mkdir -p "$SNAP"
# --link-dest 指向上份快照(硬链接未变文件,省盘)。dry-run 下也安全。
if [ -n "$PREV" ] && [ -d "$DEST/$PREV" ]; then
LINK_DEST_OPT="--link-dest=$DEST/$PREV"
echo "[link-dest] 硬链接基准: $DEST/$PREV"
fi
echo
# ----------------------------------------------------------------------------
# 逐路径 rsync 拉取(生产 → NAS 快照)
# ----------------------------------------------------------------------------
# 把逗号分隔的 paths 转成数组(去空段)
IFS=',' read -ra PATH_ARR <<< "$PATHS"
for p in "${PATH_ARR[@]}"; do
p="${p%/}" # 去尾斜杠,保证语义一致
[ -z "$p" ] && continue
name="$(basename "$p")"
target="$SNAP/$name"
mkdir -p "$target"
echo "[rsync] $HOST:$p/ → $target/"
# shellcheck disable=SC2086 # LINK_DEST_OPT 需要按词拆分
rsync "${RSYNC_FLAGS[@]}" $LINK_DEST_OPT \
"$HOST:$p/" "$target/" \
|| { echo "[FAIL] rsync 失败: $HOST:$p" >&2; exit 1; }
echo "[rsync] done: $name"
echo
done
# ----------------------------------------------------------------------------
# 保留策略:留最近 N 份,旧的删
# 只识别 YYYYMMDD 命名的子目录,绝不碰其他名字(防止误删 dest 下非快照内容)。
# ----------------------------------------------------------------------------
echo "[retention] 保留最近 $KEEP 份快照"
mapfile -t SNAPSHOTS < <(ls -1 "$DEST" 2>/dev/null | grep -E '^[0-9]{8}$' | sort -r || true)
DELETED=0
idx=0
for s in "${SNAPSHOTS[@]}"; do
idx=$((idx + 1))
if [ "$idx" -le "$KEEP" ]; then
echo " [keep] $s"
else
if [ "$DRY_RUN" -eq 1 ]; then
echo " [would-delete] $s"
else
rm -rf "${DEST:?}/$s" && echo " [deleted] $s"
fi
DELETED=$((DELETED + 1))
fi
done
echo "[retention] 本次删除/拟删: $DELETED"
echo
echo "=== 备份完成 ==="
echo " 快照: $SNAP"
[ "$DRY_RUN" -eq 1 ] && echo " dry-run,未实际写入/删除)"