备份策略改为部署前手动快照:部署合同增加手动快照模式 [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 行删除
@@ -37,6 +37,22 @@ operations require explicit approval and evidence.
`deploy/trialrun/.env.example` before deployment; `deploy/trialrun/deploy.sh` `deploy/trialrun/.env.example` before deployment; `deploy/trialrun/deploy.sh`
runs `node deploy/trialrun/validate-env-contract.js` at step 0 and must fail runs `node deploy/trialrun/validate-env-contract.js` at step 0 and must fail
if the example contract drifts. if the example contract drifts.
- Current backup policy is manual Tencent Lighthouse snapshots only, as selected
in `08-28-production-arm-backup-transfer-incident/prd.md`. Do not introduce
object storage, ARM/hyinda-20c backup or automatic snapshot rotation by default.
Before each approved production deployment, confirm the OA target and coverage
of database/attachment disks, then wait for snapshot creation to succeed.
Record target/coverage confirmation, creation time and success without resource
IDs. Check current console quota before any separately authorized old-snapshot
deletion; preserve an existing successful recovery point during rotation.
Recoverability is limited to existing snapshot times and covered disks; this
policy provides neither a daily recovery-point guarantee nor an independent copy.
- Policy selection does not prove automatic backups have stopped. Follow the
same task's reviewed transition plan: preserve the service-based deployment
identity contract, adapt backup timer checks before disabling those timers,
and separately approve remote configuration changes. Preserve historical
backups and independent public-health monitoring. Legacy backup contracts below
describe retained tooling; they do not authorize re-enabling backup services.
- After deployment, report commit hash, `/ready`, public smoke results and - After deployment, report commit hash, `/ready`, public smoke results and
whether the user can test. whether the user can test.
- Real business writes are manual unless the current Trellis task explicitly - Real business writes are manual unless the current Trellis task explicitly
@@ -2489,6 +2505,7 @@ DEPLOY_SERVER=<tencent-oa-production-target> bash deploy/trialrun/deploy-existin
- An early aggregate item fails -> all independent items still run, the fixed summary reports every FAIL, and no remote write begins. - An early aggregate item fails -> all independent items still run, the fixed summary reports every FAIL, and no remote write begins.
- Root SSH returns transport 255 -> `root_identity_unavailable`; remote 126/127 or missing probe marker -> `refresh_program_unavailable`. - Root SSH returns transport 255 -> `root_identity_unavailable`; remote 126/127 or missing probe marker -> `refresh_program_unavailable`.
- Helper runs but final service build differs -> `unit_build_not_refreshed`; builds match but any timer is disabled/inactive -> `timer_not_active`. - Helper runs but final service build differs -> `unit_build_not_refreshed`; builds match but any timer is disabled/inactive -> `timer_not_active`.
- Backup mode file `/etc/hyb-oa-backup-mode` absent -> automatic mode, all backup and ops timers must be enabled/active. Content exactly `manual_snapshot` (root-owned regular file) -> the three automatic-backup timers must be disabled/inactive, otherwise `backup_timer_not_disabled` (45); `hyb-oa-warch3-ops.timer` stays required (`timer_not_active`). Any other content, symlink or ownership -> `backup_mode_invalid` (44). In manual mode every backup `.service` must also be `inactive` or `failed`; an in-flight run -> 45. `probe` runs the same mode and timer/service checks, so a mismatch fails the pre-deploy aggregate as `refresh_program FAIL` (run the forced-command `probe` directly to read the category on stderr) before any release write; `refresh` re-checks after the release. Never mask or delete backup units: deploy identity still reads the daily backup service environment.
- A similarly named environment key such as `OLD_BUILD_SHA` matches the active value while exact `BUILD_SHA` remains stale -> treat the service as stale; build verification parses exact keys and applies the last systemd override. - A similarly named environment key such as `OLD_BUILD_SHA` matches the active value while exact `BUILD_SHA` remains stale -> treat the service as stale; build verification parses exact keys and applies the last systemd override.
- Post-deploy installer still embeds the previous build SHA, any managed timer is inactive, or the real backup/watchdog fails runtime identity or off-host freshness -> deployment recovery remains incomplete even when `/ready` is green. - Post-deploy installer still embeds the previous build SHA, any managed timer is inactive, or the real backup/watchdog fails runtime identity or off-host freshness -> deployment recovery remains incomplete even when `/ready` is green.
- A later deployment attempt is blocked by `backup_build` after `DEPLOYED_BUT_CONTRACT_REFRESH_FAILED` -> expected fail-closed behavior; repair with direct forced-command `refresh`, not another application release. - A later deployment attempt is blocked by `backup_build` after `DEPLOYED_BUT_CONTRACT_REFRESH_FAILED` -> expected fail-closed behavior; repair with direct forced-command `refresh`, not another application release.
@@ -1,5 +1,17 @@
# 生产到 ARM 备份传输中断设计 # 生产到 ARM 备份传输中断设计
## 2026-10-05 手动快照切换方案(本地适配已完成,远端切换待批准)
用户已选定仅手动快照,本节替代下方历史修复路径。用户 2026-10-05 批准后本地适配已实施并验证(结果见 implement.md);远端切换尚未执行,需另行批准。
1. 发布适配:`scripts/refresh-deployment-contract.sh` 的 `verify_timers` 当前强制要求三个备份 timer enabled/active;`deploy-existing.sh`、生产身份引导器和聚合检查仍从 daily service 读取部署身份。已增加显式、受控的手动快照模式(根属 `/etc/hyb-oa-backup-mode` 内容为 `manual_snapshot`),校验备份 timer disabled/inactive 且备份 service 已退出(inactive/failed),`probe` 部署前与 `refresh` 部署后均检查,保留身份、构建、数据目录及非备份运维 timer 的验证。不得删除单位、全局跳过 timer 检查或靠错误吞并放行。
2. 本地验证:扩展既有发布合同测试,覆盖手动模式三个备份 timer 停用可通过、意外启用失败、非备份 timer 失败仍阻断及旧模式回归。受控入口的模式配置与候选版本一并绑定,实际执行前完成可用的独立只读复核;当前 inline 限制下标记待独立复核,不虚报。
3. 远端切换范围:生产 `hyb-oa-automatic-backup-{daily,watchdog,drill-reminder}.timer` 与 ARM `hyb-oa-offhost-deadman.timer`。先核实际单位及既有运行状态;先禁用定时器阻止新任务启动,再有界等待在途 service 退出(上限预先写定),超时停止切换并报告、不强杀;最终核验含对应 service inactive/failed。ARM dead-man 由用户 sudo 停用并单独核验,生产侧 `refresh` 成功不代替 ARM 核验。源码默认名称须与现场一致,不猜测替代单位。
4. 保留 unit 文件及其环境、历史归档/partial、状态、凭据和共享告警工具,不调用带删除行为的 installer uninstall。公网健康探针与 `hyb-oa-warch3-ops.timer` 不在备份停用范围,hyinda-20c 未安装本项目备份任务,无迁移或清理动作。
5. 顺序为先完成兼容适配、再获批部署适配并停用定时器、最后只读核验与记录。执行前保留旧模式/单位启用状态作为回退依据;出现异常停止,恢复旧模式和定时器需在本次获批回滚范围内执行。不得出现脚本仍强制 active 而定时器已关闭的未处理状态。
本方案不购买存储、不创建或删除快照、不删备份、不恢复业务数据。远端具体配置、版本和回滚清单须在实施后形成可审阅结果再申请执行批准;当前选择不等于远端切换已完成。
## 设计结论 ## 设计结论
采用“先只读判因、再最小变更、最后两个自然周期验证”的事件处置路径。传输修复和容量处置属于同一个 P1 operations 任务,但每项远端写操作维持独立审批;恢复演练作为下游消费者保持阻塞,不参与本任务的根因修复。 采用“先只读判因、再最小变更、最后两个自然周期验证”的事件处置路径。传输修复和容量处置属于同一个 P1 operations 任务,但每项远端写操作维持独立审批;恢复演练作为下游消费者保持阻塞,不参与本任务的根因修复。
@@ -1,5 +1,57 @@
# 生产到 ARM 备份传输中断实施计划 # 生产到 ARM 备份传输中断实施计划
## 2026-10-05 最新决定与本地落实:仅手动快照
用户明确选择仅手动快照,不用其他厂商对象存储,也不再选 ARM/hyinda-20c 承担备份。此前对象存储选型、ARM 修复或替换、两个自然周期验收不再作为当前推进目标;原始结果保留,不勾成通过、不自动归档。
- [x] 复用本 task,更新 PRD 当前目标、design 切换方案及部署规则;不再询问备份产品选择。
- [x] 本地确认停用依赖:`scripts/refresh-deployment-contract.sh:verify_timers` 要求三个自动备份 timer 启用且运行;生产身份仍取自 daily service。不能直接卸载或停 timer 后宣称发布兼容。
- [x] 本地 task validate、Trellis governance、process baseline、敏感扫描及 diff 检查通过;docs registry 无错误,保留 22 个未登记文档的既有警告,本轮未改动 docs 或处理这些文件。
- [x] 用户 2026-10-05 批准适配后本地实施并验证:
- `scripts/refresh-deployment-contract.sh`:新增根属普通文件 `/etc/hyb-oa-backup-mode` 显式模式。文件不存在 = 原自动模式(行为不变);内容恰为 `manual_snapshot` 时要求三个自动备份 timer `disabled:inactive`(否则 exit 45 `backup_timer_not_disabled`),`hyb-oa-warch3-ops.timer` 仍须 `enabled:active`(43);其他内容/符号链接/非 root 属主 → exit 44 `backup_mode_invalid`,`probe` 同样校验。不删除、不 mask 备份 unit,部署身份仍取自 daily service。
- `deploy/trialrun/deployment-contract-lib.sh` 增加 44/45 分类;`validate-env-contract.js` 增加必需片段;部署规约 Validation Matrix 增加对应条目。
- 验证:`run-deployment-contract-refresh-contract.sh` PASS(新增自动模式下停用失败、手动模式通过/全部运行/部分运行/ops 停用/内容非法/多行/符号链接/移除模式文件回归共 10 组断言);两个源码变异(手动模式接受运行中 timer、跳过内容校验)均使测试变红;`validate-env-contract.js` exit 0;`deployment-safety-contract` 39/39、`deployment-db-lifecycle` 52/52。
- [x] 复核补充(同日):①`probe` 也执行模式与 timer/service 一致性检查,部署前即拦截,`refresh` 部署后复检;②手动模式还要求三个备份 `.service` 为 `inactive`/`failed`,在途运行 → 45。新增 7 条断言(probe 拦截运行中/部分运行、自动模式 probe 拦截停用、服务 active/activating 拒绝、failed 放行);`probe` 去掉 timer 检查、去掉 service 检查两个变异均使测试变红;合同测试、env 合同、部署安全 + DB 生命周期 91/91 通过。注意:自动模式下 timer 异常现在部署前就失败(原先只在部署后失败),属有意收紧。
- [ ] 远端切换(另批;生产需 root 安装权限,ARM 需用户 sudo)。顺序:
1. 用 `install-deployment-contract-refresh.sh install` 安装新 helper;无模式文件时仍为自动模式,forced-command `probe` 须 PASS。
2. 先 `systemctl disable --now` 三个生产备份 **timer**(只停定时器,阻止新任务启动,不动 service)。
3. 有界等待三个备份 service 变为 `inactive`/`failed`(上限预先写定,例如 90 分钟);超时则停止切换、保持现状并报告,**不强杀**。
4. 写入 `/etc/hyb-oa-backup-mode`(root:root 0644,内容 `manual_snapshot`),forced-command `refresh` 返回 PASS。
5. 由用户 sudo 在 ARM 上 `disable --now hyb-oa-offhost-deadman.timer` 并**单独核验** timer `disabled:inactive`、service 非 active;生产侧 `refresh` 成功不能代替 ARM 核验。未停 dead-man 时,生产停传后 ARM 会发一次 `archive_stale`。
- 回滚:删除模式文件,`enable --now` 三个 timer 与 dead-man,再跑 `refresh`。历史备份、unit 文件、公网健康探针均不动。
- [ ] 完成受限只读独立复核;当前 inline 模式下仅主会话检查,独立复核待完成。
- [ ] 形成固定版本、模式配置、四个备份 timer 及回滚清单,另获生产/ARM 配置变更批准后执行与核验。
运行状态:本轮未连接生产或 ARM,自动备份/告警可能仍按旧配置运行;未停用定时器、未删除历史备份、未创建快照,未提交或归档。当前选择不自动停用独立公网健康监控。手动快照执行时记录目标确认、覆盖确认、创建时间及成功状态,不记录真实资源标识。
## 2026-10-05 告警频繁的只读核验(用户当轮授权,ARM 普通账号)
背景:用户反馈群内仍频繁出现“内部 OA 运维恢复通知”及“OA 公网健康告警/恢复”。授权范围为生产与 ARM 只读核验;生产主机读取被本机工具权限策略拦截,未执行,生产侧数据全部为 NOT_RUN。
ARM 侧结果(13:30–13:45 CST,只记聚合值,不记地址/路径/日志原文):
- dead-man:timer active/enabled,10-05 06:30 service success/0。说明当日 06:30 时 ARM 最新归档未超 26h 阈值;归档完整性未复核(目录及 sudo 仍不可用)。
- 公网健康探针:timer active/enabled,13:30 那次 service 以 exit-code/1 失败,耗时约 11s(与 10s 超时一致);具体类别在 root-only 状态/journal,普通账号读不到(journal 返回“无条目”)。
- ARM → OA 公网入口:ping 三轮丢包 67% / 74% / 52%,RTT 约 143ms;HTTPS `/ready` 40 次中 11 次超时(10s)、13 次 TCP 建连 >1s、平均 3.35s。
- 对照:ARM → 其他三个国内目标 ping 0% 丢包;ARM 本机 DNS 解析约 1ms(已缓存),当前不是 DNS 阶段问题。本机开发环境经代理(假 IP 段)访问,40/40 成功,但路径不同,不能证明生产入口对普通用户正常。
判断(假设,未结案):当前公网健康告警的主要成因是 ARM 到生产入口这条线路持续丢包导致请求超时,不是 OA 应用不可用。同一条线路的丢包也可能是历史 rsync 传输中断(partial)的成因,需生产侧传输记录印证。丢包发生在 ARM 运营商一侧还是生产入口一侧尚未区分;若在生产入口一侧,真实用户也可能受影响。
未完成 / 下一步:
- 生产侧(备份/watchdog 服务结果、告警状态、容量、近 7 天告警类别与部署时间对照):需用户放开本机对生产主机只读命令的权限,或由用户手动执行。
- ARM root-only 公网健康状态文件和 journal 中的类别统计:需用户以 sudo 手动执行固定只读命令。
- 区分线路问题在哪一侧:需要第三个直连观测点(不经代理)对生产入口测丢包。
### 2026-10-05 候选替代节点 hyinda-20c 只读摸底(用户当轮授权)
- 形态:Windows 主机上的 WSL2(Ubuntu 22.04,x86_64,20 核,15 GiB 内存),经动态域名 + 非标端口映射访问;普通账号 sudo 需密码;无 Docker。
- 可用性:开机约 9 天;近两个月重启记录集中在 09-26(多次)与 08-22(多次),生命周期受 Windows 开关机/更新/WSL 实例回收影响。
- 存储:WSL ext4 显示 1 TiB 虚拟盘,实际受宿主 Windows 盘余量约束(宿主两盘分别约 167G / 88G 可用);未见 Linux 层磁盘加密,宿主 BitLocker 未知。
- 网络:节点流量(含指定公共 DNS 的解析)均被本机透明代理接管为假 IP 段,到 OA 入口 HTTPS 70/70 成功、平均约 0.13s,但不是直连测量,不能作为线路证据。
- 容量需求参照:既有记录单批次聚合约 11 MB 量级,容量和带宽对任何候选节点都不是约束;选型关键是常在线、独立、可控访问与恢复能力。
- 用户决定(2026-10-05):不购买轻量“自动备份”(备份点按 60GB 盘容量预付至实例到期,约 983 元);采用**每次部署前手动创建轻量免费快照**(配额 2,满则删最旧),已写入 `.trellis/spec/hyb-oa/deployment-trialrun.md` Rules。快照随实例销毁而删除,不替代实例外副本;实例外副本(另一厂商对象存储)与 ARM 链路退役待用户另行决定。用户确认控制台中名为“Windows Server”的轻量实例即 OA 生产(实际为 Linux,仅实例名称历史遗留),快照对象即此实例。
- 结论:不建议 hyinda-20c 作为主要异机备份节点(WSL 生命周期、代理接管网络、虚拟盘依赖宿主、无加密证据);可作为额外副本候选。主要节点建议改评国内云的独立厂商对象存储或小型云主机,待用户决策。
## 2026-09-17 结案缺口与最小补证方案(本地整理,未执行远端观察) ## 2026-09-17 结案缺口与最小补证方案(本地整理,未执行远端观察)
@@ -1,5 +1,18 @@
# 生产到 ARM 备份传输中断 # 生产到 ARM 备份传输中断
## 2026-10-05 当前决策:仅使用手动快照
用户明确决定不使用其他厂商对象存储、ARM 或 hyinda-20c 备份,只使用已确认 OA 生产轻量实例的手动快照。此决策取代本任务继续修复异机链路、选型迁移及两个自然备份周期验收的目标;以下原始要求和 AC 保留为历史,未完成项不追认为通过。关联 `08-23-isolated-backup-restore-drill` 的历史部分恢复结果不变,本决策不授权继续真实恢复。
- 当前备份方式为部署前手动快照;确认目标、磁盘覆盖和创建成功后再部署,不新增自动快照脚本或其他副本方案。恢复范围受现存成功快照的时间点与覆盖磁盘限制,不承诺每日恢复点或实例外副本。
- 选择备份方式与实际停用分开记录。本轮落实本地合同;发布脚本适配、生产/ARM 定时任务停用方案见 design/implement,非微小代码实施和远端配置变更待方案批准。
- 历史备份、快照、凭据和 unit 文件不因本决策自动删除;公网健康监控是独立功能,不把“停用备份”解释为关闭全部监控。
### 当前验收
- [x] S1:当前选择已同步到任务及部署规约,本地文档/治理检查通过;旧未验项与当前决策明确区分。文档审计保留既有未登记文件警告,详见 implement。
- [ ] S2:获批的发布适配与备份任务停用完成,停用状态及发布兼容性有据;此前不得称“已经只运行手动快照方案”。
## Goal ## Goal
恢复腾讯云生产到 ARM 的自动异机备份链,消除持续扩大的异机恢复点缺口,同时把生产备份落盘容量恢复到可持续范围,并证明修复不是一次偶然成功。 恢复腾讯云生产到 ARM 的自动异机备份链,消除持续扩大的异机恢复点缺口,同时把生产备份落盘容量恢复到可持续范围,并证明修复不是一次偶然成功。
@@ -12,7 +12,7 @@
"assignee": "shunli", "assignee": "shunli",
"createdAt": "2026-08-28", "createdAt": "2026-08-28",
"completedAt": null, "completedAt": null,
"branch": null, "branch": "main",
"base_branch": "main", "base_branch": "main",
"worktree_path": null, "worktree_path": null,
"commit": null, "commit": null,
@@ -81,6 +81,8 @@ deployment_contract_refresh_category() {
255) printf 'root_identity_unavailable' ;; 255) printf 'root_identity_unavailable' ;;
126|127) printf 'refresh_program_unavailable' ;; 126|127) printf 'refresh_program_unavailable' ;;
43) printf 'timer_not_active' ;; 43) printf 'timer_not_active' ;;
44) printf 'backup_mode_invalid' ;;
45) printf 'backup_timer_not_disabled' ;;
*) printf 'unit_build_not_refreshed' ;; *) printf 'unit_build_not_refreshed' ;;
esac esac
} }
+3
查看文件
@@ -751,6 +751,9 @@ for (const requiredFragment of [
'refresh_program_integrity_invalid', 'refresh_program_integrity_invalid',
'unit_build_not_refreshed', 'unit_build_not_refreshed',
'timer_not_active', 'timer_not_active',
'backup_mode_invalid',
'backup_timer_not_disabled',
'manual_snapshot',
'systemctl daemon-reload', 'systemctl daemon-reload',
]) { ]) {
if (!deploymentRefreshHelperContent.includes(requiredFragment)) { if (!deploymentRefreshHelperContent.includes(requiredFragment)) {
@@ -133,9 +133,42 @@ case "$action" in
esac esac
;; ;;
daemon-reload) exit 0 ;; daemon-reload) exit 0 ;;
is-enabled) echo enabled ;; is-enabled)
unit="${2:-}"
if [[ "$unit" == hyb-oa-automatic-backup-* ]]; then
case "${FIXTURE_BACKUP_TIMERS:-running}" in
disabled) echo disabled; exit 1 ;;
mixed)
if [[ "$unit" == *watchdog* ]]; then echo enabled; else echo disabled; exit 1; fi
;;
*) echo enabled ;;
esac
else
echo enabled
fi
;;
is-active) is-active)
unit="${2:-}"
if [[ "$unit" == *.service ]]; then
if [[ "$unit" == hyb-oa-automatic-backup-daily.service \
&& -n "${FIXTURE_DAILY_SERVICE_STATE:-}" ]]; then
echo "$FIXTURE_DAILY_SERVICE_STATE"
[[ "$FIXTURE_DAILY_SERVICE_STATE" == "active" ]] || exit 3
exit 0
fi
echo inactive; exit 3
fi
if [[ "${FIXTURE_TIMER_INACTIVE:-0}" == "1" ]]; then echo inactive; exit 3; fi if [[ "${FIXTURE_TIMER_INACTIVE:-0}" == "1" ]]; then echo inactive; exit 3; fi
if [[ "$unit" == hyb-oa-automatic-backup-* ]]; then
case "${FIXTURE_BACKUP_TIMERS:-running}" in
disabled) echo inactive; exit 3 ;;
mixed)
if [[ "$unit" == *watchdog* ]]; then echo active; else echo inactive; exit 3; fi
;;
esac
elif [[ "${FIXTURE_OPS_TIMER_INACTIVE:-0}" == "1" ]]; then
echo inactive; exit 3
fi
echo active echo active
;; ;;
*) exit 1 ;; *) exit 1 ;;
@@ -162,6 +195,57 @@ unsupported_status=$?
set -e set -e
[[ "$unit_status" -eq 42 && "$timer_status" -eq 43 && "$unsupported_status" -eq 64 ]] [[ "$unit_status" -eq 42 && "$timer_status" -eq 43 && "$unsupported_status" -eq 64 ]]
run_refresh_status() {
local command="$1"
shift
set +e
env HYB_CONTRACT_REFRESH_TEST_ROOT="$FIXTURE_ROOT" "$@" \
SSH_ORIGINAL_COMMAND="$command" bash "$HELPER" >/dev/null 2>&1
local status=$?
set -e
printf '%s' "$status"
}
mode_file="$FIXTURE_ROOT/etc/hyb-oa-backup-mode"
# Automatic mode (no mode file): disabled backup timers stay fail-closed,
# both before (probe) and after (refresh) a release.
[[ "$(run_refresh_status refresh FIXTURE_BACKUP_TIMERS=disabled)" -eq 43 ]]
[[ "$(run_refresh_status probe FIXTURE_BACKUP_TIMERS=disabled)" -eq 43 ]]
[[ "$(run_refresh_status probe FIXTURE_TIMER_INACTIVE=1)" -eq 43 ]]
printf 'manual_snapshot\n' >"$mode_file"
manual_output="$(HYB_CONTRACT_REFRESH_TEST_ROOT="$FIXTURE_ROOT" FIXTURE_BACKUP_TIMERS=disabled \
SSH_ORIGINAL_COMMAND=refresh bash "$HELPER")"
[[ "$manual_output" == '[deployment-contract-refresh][PASS] refreshed' ]]
[[ "$(run_refresh_status probe FIXTURE_BACKUP_TIMERS=disabled)" -eq 0 ]]
# Manual mode must not silently accept running or partially running backup timers,
# and the pre-deploy probe must reject the mismatch before any release write.
[[ "$(run_refresh_status refresh)" -eq 45 ]]
[[ "$(run_refresh_status probe)" -eq 45 ]]
[[ "$(run_refresh_status refresh FIXTURE_BACKUP_TIMERS=mixed)" -eq 45 ]]
[[ "$(run_refresh_status probe FIXTURE_BACKUP_TIMERS=mixed)" -eq 45 ]]
# Disabled timers with an in-flight backup run are not a finished switch.
[[ "$(run_refresh_status refresh FIXTURE_BACKUP_TIMERS=disabled FIXTURE_DAILY_SERVICE_STATE=active)" -eq 45 ]]
[[ "$(run_refresh_status probe FIXTURE_BACKUP_TIMERS=disabled FIXTURE_DAILY_SERVICE_STATE=activating)" -eq 45 ]]
[[ "$(run_refresh_status refresh FIXTURE_BACKUP_TIMERS=disabled FIXTURE_DAILY_SERVICE_STATE=failed)" -eq 0 ]]
# Non-backup ops timer is still required in manual mode.
[[ "$(run_refresh_status refresh FIXTURE_BACKUP_TIMERS=disabled FIXTURE_OPS_TIMER_INACTIVE=1)" -eq 43 ]]
printf 'manual\n' >"$mode_file"
[[ "$(run_refresh_status refresh FIXTURE_BACKUP_TIMERS=disabled)" -eq 44 ]]
[[ "$(run_refresh_status probe FIXTURE_BACKUP_TIMERS=disabled)" -eq 44 ]]
printf 'manual_snapshot\nautomatic\n' >"$mode_file"
[[ "$(run_refresh_status refresh FIXTURE_BACKUP_TIMERS=disabled)" -eq 44 ]]
rm -f -- "$mode_file"
printf 'manual_snapshot\n' >"$FIXTURE_ROOT/backup-mode-target"
ln -s "$FIXTURE_ROOT/backup-mode-target" "$mode_file"
[[ "$(run_refresh_status refresh FIXTURE_BACKUP_TIMERS=disabled)" -eq 44 ]]
rm -f -- "$mode_file"
[[ "$(run_refresh_status refresh)" -eq 0 ]]
[[ "$(deployment_contract_refresh_category 44 '')" == "backup_mode_invalid" ]]
[[ "$(deployment_contract_refresh_category 45 '')" == "backup_timer_not_disabled" ]]
public_key_file="$FIXTURE_ROOT/refresh.pub" public_key_file="$FIXTURE_ROOT/refresh.pub"
key_blob='AAAAC3NzaC1lZDI1NTE5AAAAIFixturerefreshkeymaterial000000000000' key_blob='AAAAC3NzaC1lZDI1NTE5AAAAIFixturerefreshkeymaterial000000000000'
printf 'ssh-ed25519 %s fixture-refresh\n' "$key_blob" >"$public_key_file" printf 'ssh-ed25519 %s fixture-refresh\n' "$key_blob" >"$public_key_file"
+61 -5
查看文件
@@ -12,10 +12,17 @@ SERVICES=(
hyb-oa-automatic-backup-drill-reminder.service hyb-oa-automatic-backup-drill-reminder.service
hyb-oa-warch3-ops.service hyb-oa-warch3-ops.service
) )
TIMERS=( BACKUP_TIMERS=(
hyb-oa-automatic-backup-daily.timer hyb-oa-automatic-backup-daily.timer
hyb-oa-automatic-backup-watchdog.timer hyb-oa-automatic-backup-watchdog.timer
hyb-oa-automatic-backup-drill-reminder.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 hyb-oa-warch3-ops.timer
) )
CONTRACT_SERVICE="hyb-oa-automatic-backup-daily.service" CONTRACT_SERVICE="hyb-oa-automatic-backup-daily.service"
@@ -47,6 +54,11 @@ if [[ "$(id -u)" != "0" ]]; then
fi fi
UNIT_ROOT="${ROOT_PREFIX}/etc/systemd/system" 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" LOCK_FILE="${ROOT_PREFIX}/run/lock/hyb-oa-deployment-contract-refresh.lock"
EXPECTED_TARGET="${ROOT_PREFIX}/usr/local/sbin/hyb-oa-deployment-contract-refresh" EXPECTED_TARGET="${ROOT_PREFIX}/usr/local/sbin/hyb-oa-deployment-contract-refresh"
@@ -160,25 +172,69 @@ verify_builds() {
done 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() { verify_timers() {
local timer local timer
for timer in "${TIMERS[@]}"; do for timer in "${OPS_TIMERS[@]}"; do
[[ "$(systemctl is-enabled "$timer")" == "enabled" \ [[ "$(timer_state "$timer")" == "enabled:active" ]] || fail 43 timer_not_active
&& "$(systemctl is-active "$timer")" == "active" ]] \ done
|| fail 43 timer_not_active 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 done
} }
probe() { probe() {
validate_self validate_self
validate_fixed_units validate_fixed_units
resolve_backup_mode
resolve_active_build resolve_active_build
# Fail before any release write when timers disagree with the backup mode.
verify_timers
echo "[deployment-contract-refresh][PASS] probe" echo "[deployment-contract-refresh][PASS] probe"
} }
refresh() { refresh() {
validate_self validate_self
validate_fixed_units validate_fixed_units
resolve_backup_mode
resolve_active_build resolve_active_build
mkdir -p "$(dirname "$LOCK_FILE")" mkdir -p "$(dirname "$LOCK_FILE")"
exec 9>"$LOCK_FILE" exec 9>"$LOCK_FILE"