备份策略改为部署前手动快照:部署合同增加手动快照模式 [08-28-production-arm-backup-transfer-incident]

- 用户决定仅用部署前手动轻量快照,不再推进对象存储、ARM/hyinda-20c 备份
- refresh-deployment-contract 新增 /etc/hyb-oa-backup-mode 显式模式:
  无文件保持原自动模式;manual_snapshot 要求三个备份 timer 停用且备份
  service 已退出,ops timer 仍须运行;内容/属主/符号链接异常 fail-closed
- probe 部署前与 refresh 部署后均校验模式与 timer/service 一致性
- 新增 44 backup_mode_invalid / 45 backup_timer_not_disabled 分类及合同测试
- 记录 10-05 ARM 与 hyinda-20c 只读核验结果、远端切换顺序与回滚
- 远端切换未执行,需另行批准

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
2026-10-05 17:28:45 +08:00
共同撰写人 Claude Opus 5.5
父节点 9cbefdf243
当前提交 f6d1025d85
共修改 9 个文件,包含 246 行新增和 7 行删除
+61 -5
查看文件
@@ -12,10 +12,17 @@ SERVICES=(
hyb-oa-automatic-backup-drill-reminder.service
hyb-oa-warch3-ops.service
)
TIMERS=(
BACKUP_TIMERS=(
hyb-oa-automatic-backup-daily.timer
hyb-oa-automatic-backup-watchdog.timer
hyb-oa-automatic-backup-drill-reminder.timer
)
BACKUP_SERVICES=(
hyb-oa-automatic-backup-daily.service
hyb-oa-automatic-backup-watchdog.service
hyb-oa-automatic-backup-drill-reminder.service
)
OPS_TIMERS=(
hyb-oa-warch3-ops.timer
)
CONTRACT_SERVICE="hyb-oa-automatic-backup-daily.service"
@@ -47,6 +54,11 @@ if [[ "$(id -u)" != "0" ]]; then
fi
UNIT_ROOT="${ROOT_PREFIX}/etc/systemd/system"
# Absent file = automatic backup mode (all backup timers must run). The only
# accepted content is "manual_snapshot": backup timers must then be disabled
# and inactive, because backups are manual Lighthouse snapshots before deploys.
BACKUP_MODE_FILE="${ROOT_PREFIX}/etc/hyb-oa-backup-mode"
BACKUP_MODE=""
LOCK_FILE="${ROOT_PREFIX}/run/lock/hyb-oa-deployment-contract-refresh.lock"
EXPECTED_TARGET="${ROOT_PREFIX}/usr/local/sbin/hyb-oa-deployment-contract-refresh"
@@ -160,25 +172,69 @@ verify_builds() {
done
}
resolve_backup_mode() {
local content
if [[ ! -e "$BACKUP_MODE_FILE" && ! -L "$BACKUP_MODE_FILE" ]]; then
BACKUP_MODE="automatic"
return 0
fi
[[ -f "$BACKUP_MODE_FILE" && ! -L "$BACKUP_MODE_FILE" ]] \
|| fail 44 backup_mode_invalid
if [[ "$TEST_MODE" == "0" ]]; then
[[ "$(stat -c %u:%g "$BACKUP_MODE_FILE")" == "0:0" ]] \
&& [[ "$(stat -c %a "$BACKUP_MODE_FILE")" =~ ^(644|600|444|400)$ ]] \
|| fail 44 backup_mode_invalid
fi
content="$(<"$BACKUP_MODE_FILE")"
[[ "$content" == "manual_snapshot" ]] || fail 44 backup_mode_invalid
BACKUP_MODE="manual_snapshot"
}
timer_state() {
local timer="$1"
printf '%s:%s' \
"$(systemctl is-enabled "$timer" 2>/dev/null || true)" \
"$(systemctl is-active "$timer" 2>/dev/null || true)"
}
verify_timers() {
local timer
for timer in "${TIMERS[@]}"; do
[[ "$(systemctl is-enabled "$timer")" == "enabled" \
&& "$(systemctl is-active "$timer")" == "active" ]] \
|| fail 43 timer_not_active
for timer in "${OPS_TIMERS[@]}"; do
[[ "$(timer_state "$timer")" == "enabled:active" ]] || fail 43 timer_not_active
done
for timer in "${BACKUP_TIMERS[@]}"; do
if [[ "$BACKUP_MODE" == "manual_snapshot" ]]; then
[[ "$(timer_state "$timer")" == "disabled:inactive" ]] \
|| fail 45 backup_timer_not_disabled
else
[[ "$(timer_state "$timer")" == "enabled:active" ]] || fail 43 timer_not_active
fi
done
[[ "$BACKUP_MODE" == "manual_snapshot" ]] || return 0
# A disabled timer does not stop an in-flight run; the switch is complete
# only after every backup service has exited (a failed last run still counts).
local service state
for service in "${BACKUP_SERVICES[@]}"; do
state="$(systemctl is-active "$service" 2>/dev/null || true)"
[[ "$state" == "inactive" || "$state" == "failed" ]] \
|| fail 45 backup_timer_not_disabled
done
}
probe() {
validate_self
validate_fixed_units
resolve_backup_mode
resolve_active_build
# Fail before any release write when timers disagree with the backup mode.
verify_timers
echo "[deployment-contract-refresh][PASS] probe"
}
refresh() {
validate_self
validate_fixed_units
resolve_backup_mode
resolve_active_build
mkdir -p "$(dirname "$LOCK_FILE")"
exec 9>"$LOCK_FILE"