54f9ab4c4f
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启动).
189 lines
6.4 KiB
Bash
Executable File
189 lines
6.4 KiB
Bash
Executable File
#!/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,未实际写入/删除)"
|