fix(recovery): capture reboot lessons and gitea backup coverage
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Failing after 55s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped

This commit is contained in:
Your Name
2026-06-30 19:00:02 +08:00
parent 76fee33e1b
commit 4e909b2118
6 changed files with 174 additions and 6 deletions

View File

@@ -50328,3 +50328,26 @@ production browser smoke:
**仍需驗證**
- 跑 focused API tests、py_compile、ruff、Gitea runner pressure guard、Gitea secret step guard 與 `git diff --check`
- 推送後等待 deploy marker再讀回 priority endpoint 與新 preflight endpoint確認 production 顯示 recovery preflight 而非 manual/default-human 終局。
## 2026-06-30 — 19:15 全主機重啟後版本 / freshness / Gitea 備份完整性 readback
**完成內容**
- 重新執行 full-stack cold-start readback`PASS=64 WARN=6 BLOCKED=6`。目前不可宣稱全服務已在 10 分鐘內恢復;主要 blocker 包含 110 registry / SSH、K3s registry pull refused、Harbor / Registry / SigNoz public 502/TLS。
- 驗證 AWOOOI production 版本不是最新source / Gitea main 為 `adc0d8816`production runtime readback 仍為 `7890778b83`,且新 Stock recovery preflight endpoint 在 production 回 404。
- 修復 Tsenyang public 502188 current release `/home/ollama/tsenyang-website-releases/b369ed8` 缺 runtime image / container以現有 release bounded build `tsenyang-website:latest``docker compose up -d --no-build`local `127.0.0.1:3000``www.tsenyang.com``tsenyang.com` 均回 200。
- 驗證 StockPlatform 不是資料最新public `/healthz` / `/api/healthz` 為 200`/api/v1/system/freshness``/api/v1/system/ingestion``status=not_configured`、blocker `postgres_not_ready`
- 驗證 Gitea「repo 消失」需分層判讀Gitea version API 回 `1.25.5`public repo search 僅列 4 個 public repos`stockplatform-v2` public page/API 回 404但 internal `git ls-remote http://192.168.0.110:3001/wooo/stockplatform-v2.git` 成功,表示不能直接宣稱 Git objects 全消失。
- 發現重大備份缺口188 `/home/ollama/backup/110/gitea` 為空;既有 `backup_from_110` freshness 只證明 rsync 心跳,不證明 Gitea 子樹 / dump / repo count 完整。
- 更新 `REBOOT-POST-START-QUICK-CHECK.md` v1.19 與 reboot workplan把本次事故修復順序、Tsenyang 502 經驗、Stock `postgres_not_ready`、Gitea 分層查核、Gitea backup completeness 缺口沉澱為正式 SOP。
- 補 `backup-health-textfile-exporter.py`188 新增 `gitea_repo_mirror_from_110` subtree freshness / sample count 與 service coverage domain metric。
- 已將新版 exporter 受控套用到 188 `/home/ollama/scripts/backup-health-textfile-exporter.py` 並刷新 `/home/ollama/node_exporter_textfiles/backup_health.prom`live metric 顯示 `gitea_repo_mirror_from_110` timestamp `0`、snapshot_count `0``awoooi_backup_coverage_domain_fresh{host="188",domain="service"} 0`
- 補 `alerts-unified.yml`:新增 188 backup coverage metric missing alert並讓 `BackupCoverageDomainStale` 對所有 host 的 coverage domain 生效。
**仍維持**
- 沒有讀 secret / token / `.env` / raw sessions / SQLite / auth。
- 沒有使用 GitHub / gh / GitHub API / GitHub Actions。
- 沒有重啟主機,沒有 Docker daemon / Nginx / K3s / DB restart沒有 DB write / restore / prune。
**下一步**
- 先恢復 110 SSH read-only command path才能驗證 Stock DB/schema、Gitea dump、110 backup completeness。
- 將 188 Gitea subtree backup metric 部署到 runtime textfile exporter 後,確認 `awoooi_backup_coverage_domain_fresh{host="188",domain="service"}` 能在 Gitea 子樹空目錄時告警。

View File

@@ -1,8 +1,8 @@
# 主機重啟後一頁式總檢查
> Version: v1.18
> Last updated: 2026-06-27 Asia/Taipei
> Scope: 110 / 120 / 121 / 188 post-reboot service recovery. 112 Kali / Wazuh / active scan 不屬於本流程
> Version: v1.19
> Last updated: 2026-06-30 Asia/Taipei
> Scope: 99 / 110 / 111 / 112 / 120 / 121 / 188 post-reboot service recovery. 112 Kali / Wazuh / active scan 只做可達性與託管狀態 evidence不做 active scan
---
@@ -19,6 +19,28 @@
3. 資料與備份是否新鮮。
4. 有哪些不能宣稱完成。
### 2026-06-30 全主機重啟事故沉澱
本次重啟後不得再用單一路由或單一備份心跳宣稱恢復,必須固定讀回下列證據。
| 類別 | 2026-06-30 讀回事實 | SOP 判讀 |
|------|----------------------|----------|
| AWOOOI production 版本 | Gitea `main=adc0d8816`production readback 仍為 `7890778b83`;新 `stockplatform-public-api-controlled-recovery-preflight` route 在 production 回 404。 | 「產品都是最新版本」不可成立;必須先完成 deploy marker / runtime SHA / endpoint readback 三者一致。 |
| Full-stack cold-start | `full-stack-cold-start-check.sh --monitor-read-only --no-color``PASS=64 WARN=6 BLOCKED=6`110 registry / SSH、K3s registry pull refused、Harbor / Registry / SigNoz 502/TLS 為 blocker。 | 不可宣稱 10 分鐘全服務恢復;先修第一個 cold-start blocker再重跑同一命令。 |
| Tsenyang 502 | `www.tsenyang.com` / `tsenyang.com` 起初 HTTP 502188 release `/home/ollama/tsenyang-website-releases/b369ed8` 有 source`tsenyang-website:latest` image 缺失、container 未啟動。 | 使用者可見 502 是 P0可用現有 release 做 bounded `docker build --quiet -t tsenyang-website:latest .` + `docker compose up -d --no-build`,再以 local 3000 與 public 200 驗證。 |
| Tsenyang 修復結果 | image `sha256:e25e0acb...` 建成container `tsenyang-website` runninglocal `127.0.0.1:3000``www.tsenyang.com``tsenyang.com` 均回 200。 | 下次若同樣 502先查 image/container/release symlink不先動 Nginx、DNS、DB 或 host reboot。 |
| StockPlatform freshness | public `/healthz``/api/healthz` 回 200`/api/v1/system/freshness``/api/v1/system/ingestion``status=not_configured`、blocker `postgres_not_ready`。 | service up 不等於資料最新Stock 必須修 PostgreSQL schema/readiness 或資料 source readback禁止 fake freshness、manual DB row、restore/prune。 |
| 110 control path | ping / SSH port 可達,但 `wooo@192.168.0.110` SSH command timeout。 | 110 control path 是 P0 blocker未恢復前不可宣稱 Stock DB/schema、Gitea dump、110 backup completeness 已現場驗證。 |
| Gitea visibility | `https://gitea.wooo.work/api/v1/version` 回 200 / `1.25.5`public repo search 只列 4 個 public repos`stockplatform-v2` public page/API 404但 direct internal Git remote `http://192.168.0.110:3001/wooo/stockplatform-v2.git``ls-remote`。 | 不能直接說 repo 全消失;要區分 public visibility、private/auth visibility、內網 Git remote、server-side storage 四層。 |
| Gitea backup | 188 `/home/ollama/backup/110/gitea` 目前為空;舊 `backup_from_110` freshness 只證明 rsync 心跳,不證明 Gitea 子樹或 dump 完整。 | 備份監控必須新增 Gitea subtree freshness / sample count空目錄是 P0 backup completeness blocker。 |
本次事故後新增固定判讀:
- `public API 200` 只能代表 route / process alive要宣稱資料最新必須讀各產品 freshness / ingestion / deploy marker。
- `backup_from_110 fresh=1` 只能代表整體 rsync 成功Gitea、Harbor、DB、網站、logs 等 domain 必須有獨立 freshness 或 sample count。
- Gitea repo 疑似消失時,順序固定為 `public search``repo page/API``GIT_TERMINAL_PROMPT=0 git ls-remote <internal-gitea-url>` → server-side storage / dump / backup readback不得先 restore 或 delete。
- 使用者可見 502 先啟用 L0 maintenance fallback 或恢復單站 container若 edge/99/188 整段不可達,才進 L1 external fallback / CDN。
---
## 2. 絕對判定規則
@@ -196,7 +218,14 @@ curl -k -sS https://stock.wooo.work/api/v1/system/freshness
- `core.price_daily``core.chips_daily``core.market_index_daily.tw` 必須是 `ok`
- `blockers` 不可有 `core_margin_short_daily_missing``ai_recommendations_stale` 或其他資料閘門阻擋。
`stock.wooo.work``/healthz``/api/healthz` 皆為 200 只代表服務活著;`/api/v1/system/freshness``blocked` 時,不可宣稱 StockPlatform 資料最新。
`stock.wooo.work``/healthz``/api/healthz` 皆為 200 只代表服務活著;`/api/v1/system/freshness``blocked``not_configured` 時,不可宣稱 StockPlatform 資料最新。
若 freshness / ingestion 回 `status=not_configured` 且 blocker 為 `postgres_not_ready`
- 先確認 `/api/healthz` 是否為 `200`;若是,分類為 API alive + data/schema not ready。
- 再確認 110 control path`ssh -o BatchMode=yes -o ConnectTimeout=10 wooo@192.168.0.110 "hostname; uptime"`
- 若 SSH timeout只能記錄 `stockplatform_postgres_readiness_blocked_by_110_control_path`,不得用 DB restore、manual rows 或重啟主機處理。
- 110 control path 恢復後,才在正式 `/home/wooo/stockplatform-v2` 做 read-only compose / DB schema / migration status readback再決定是否進 controlled apply。
若 freshness blocker 是 `core_margin_short_daily_missing` / `ai_recommendations_stale`,先分層判斷:
@@ -226,6 +255,32 @@ curl -k -sS https://stock.wooo.work/api/v1/system/freshness
`escrow_missing>0` 時,服務可 green但 DR 不可 green。
### Step 5A - Gitea repo / backup completeness gate
Gitea UI 或 public route 看不到 repo 時,先用分層 readback不可直接判定 repo 消失:
```bash
curl -ksS https://gitea.wooo.work/api/v1/version
curl -ksS 'https://gitea.wooo.work/api/v1/repos/search?limit=50'
GIT_TERMINAL_PROMPT=0 timeout 20 git ls-remote --heads http://192.168.0.110:3001/wooo/stockplatform-v2.git
```
判定:
- `api/v1/version=200` 代表 Gitea process alive。
- public search 只列 public repo不代表 private/internal repo 不存在。
- public page/API `404` 加上 internal `git ls-remote` 成功,代表 public visibility / auth / route 層問題,不是 Git object 全消失。
- internal `git ls-remote` 也失敗時,才升級 server-side storage / Gitea DB / repo path readback仍不得直接 restore 或 delete。
Gitea backup completeness 必須同時看:
```bash
ssh ollama@192.168.0.188 'find /home/ollama/backup/110/gitea -maxdepth 3 -type f | head'
ssh ollama@192.168.0.188 'grep -E "gitea_repo_mirror_from_110|awoooi_backup_coverage_domain" /home/ollama/node_exporter_textfiles/backup_health.prom'
```
`backup_from_110` fresh 但 `/home/ollama/backup/110/gitea` 空,必須視為 `gitea_backup_completeness_blocked`;只能先修 backup coverage / dump / rsync include再做 restore drill。不得用「每日備份心跳正常」覆蓋 Gitea 子樹空目錄。
### Step 6 - Public routes 只作輔助證據
```bash

View File

@@ -9,6 +9,28 @@
## 1. Current Verdict
### 2026-06-30 全主機重啟後 P0 工作隊列
本段覆蓋舊的「單次重啟後人工排查」做法。所有後續狀態回報必須依此順序推進;噪音若會遮蔽 P0就掛回同一列不另開支線。
| 優先 | 狀態 | 工作項 | 2026-06-30 證據 | 下一步 / 完成條件 |
|------|------|--------|------------------|-------------------|
| P0-1 | BLOCKED | 全主機 cold-start / 10 分鐘自動恢復 SLO | `full-stack-cold-start-check.sh --monitor-read-only --no-color``PASS=64 WARN=6 BLOCKED=6`110 registry / SSH、K3s registry pull refused、Harbor / Registry / SigNoz 502/TLS 仍 blocked。 | 先修第一個 cold-start blocker重跑同一 scorecard 到 `BLOCKED=0`;不可只用 route 200 宣稱恢復。 |
| P0-2 | DONE_THIS_INCIDENT | 使用者可見 502Tsenyang | `www.tsenyang.com` / `tsenyang.com` 由 502 恢復為 200188 `tsenyang-website` container runninglocal `127.0.0.1:3000` 回 200。 | 下次同類 502 先查 release symlink / image / container不先動 Nginx、DNS、DB、主機重啟。 |
| P0-3 | BLOCKED | StockPlatform data freshness | public `/healthz``/api/healthz` 回 200freshness / ingestion 回 `not_configured``postgres_not_ready`。 | 恢復 110 control path 後read-only 查 `/home/wooo/stockplatform-v2` compose / DB schema / migration status禁止 fake freshness、manual DB rows、restore/prune。 |
| P0-4 | BLOCKED | AWOOOI production 版本最新性 | source/Gitea `main=adc0d8816`production readback 仍為 `7890778b83`;新 Stock preflight endpoint production 404。 | 補 deploy marker / runtime SHA / endpoint readback 一致;未一致前不可宣稱 AWOOOI 最新。 |
| P0-5 | BLOCKED | 110 control path | ping / SSH port 可達,但 SSH command timeout。 | 恢復 SSH read-only command path完成後才能驗證 Stock DB、Gitea dump、110 backup completeness。 |
| P0-6 | BLOCKED_BACKUP_COMPLETENESS | Gitea repo visibility 與完整備份 | Gitea version API 200public repo search 只列 4 個 public repo`stockplatform-v2` public page/API 404但 internal `git ls-remote` 成功188 `/home/ollama/backup/110/gitea` 為空。 | 新增 188 `gitea_repo_mirror_from_110` subtree freshness metric 與 188 coverage alert再補實際 Gitea dump / mirror / restore drill readback。 |
| P0-7 | SOURCE_READY_RUNTIME_BLOCKED | 99 VMware / VM autostart | repo 已有 `windows99-vmware-autostart.ps1`,但 99 SSH/WinRM 尚未可用VM host 111 仍不可達。 | 恢復 99 可控通道或由 console 套用腳本;完成後讀回 111/188/120/121/112 boot evidence。 |
| P0-8 | SOURCE_READY_RUNTIME_BLOCKED | 502 maintenance fallback / Telegram / backup alert | L0/L1 fallback runbook、Nginx snippet、reboot / backup alert rules 已在 sourceruntime 尚需部署與外部 L1 provider readback。 | L0 以測試 vhost 驗證 `X-AWOOOI-Fallback`L1 需外部雲端/CDN probeTelegram 以脫敏 alert receipt 驗證。 |
本次核心經驗:
- Gitea public UI/API 只代表 public visibilityprivate/internal repo 必須用 authenticated inventory 或 internal `git ls-remote` 分層驗證。
- 備份心跳 fresh 不等於完整備份Gitea 子樹 / dump / repo count / restore drill 必須單獨告警。
- 使用者可見 502 優先於資料 freshness先恢復靜態/容器服務,再回到資料層與版本一致性。
- 版本最新性要同時看 source SHA、deploy marker、runtime SHA 與 public endpoint不能只看 Gitea main。
| Area | Status | Completion | Evidence |
|------|--------|------------|----------|
| Overall recovery readiness | SERVICE_BLOCKED_MOMO_SOURCE_ABSENCE | 96% | 2026-06-27 11:51 live revalidation 覆蓋 02:42 舊綠燈判讀。`post-reboot-readiness-summary.sh --no-color` artifact `/tmp/awoooi-post-reboot-readiness-20260627-115046/summary.txt` 回傳 `SERVICE_GREEN=0``POST_START_RESULT=BLOCKED``POST_START_BLOCKED=2``BACKUP_CORE_GREEN=1``HOST_188_HYGIENE_BLOCKED=0``STOCK_FRESHNESS_STATUS=ok``STOCK_LATEST_TRADING_DATE=2026-06-26``ESCROW_MISSING_COUNT=5``WAZUH_MANAGER_REGISTRY_ACCEPTED=0`。本輪 188 `momo_pg_daily` configured drift 已再次修復;`backup-status``core_blockers=0``configured_missing_188=0`。K3s / ArgoCD readback120 / 121 皆 `Ready``awoooi-prod Synced / Healthy`api/web/worker pods Running。MOMO 服務健康但資料 freshness blocked正式補跑 import 回 `file_count=0`Drive metadata 回 `DRIVE_INTAKE_COUNT=0`、archive / global latest `2026-06-25T04:21:47Z`latest import job `57` clean completed 且資料範圍只到 `2026-06-24`。不可手寫 DB 或用舊 archive 假裝更新;需等新來源檔進正式 intake 後再匯入。DR 仍因 credential escrow 缺 5 不能宣稱 completeWazuh manager registry accepted 仍為 0。 |

View File

@@ -1487,14 +1487,30 @@ groups:
description: "backup-health exporter 沒有輸出 host/database/website/service/package/tool/log 聚合覆蓋指標,無法快速判斷完整備份是否還在運作。"
runbook: "部署新版 scripts/ops/backup-health-textfile-exporter.py刷新 /home/wooo/node_exporter_textfiles/backup_health.prom。"
- alert: BackupCoverageDomainMetricMissing188
expr: absent(awoooi_backup_coverage_domain_expected_info{host="188"})
for: 20m
labels:
severity: warning
layer: host-backup
component: backup-coverage
host: "188"
team: ops
alert_category: infrastructure
notification_type: TYPE-1
auto_repair: "false"
annotations:
summary: "188 備份覆蓋 domain 指標缺失"
description: "backup-health exporter 沒有輸出 188 的 service 備份覆蓋指標,無法確認 110 rsync 後 Gitea 等關鍵子樹是否真的存在。"
runbook: "部署新版 scripts/ops/backup-health-textfile-exporter.py 到 188刷新 /home/ollama/node_exporter_textfiles/backup_health.prom。"
- alert: BackupCoverageDomainStale
expr: awoooi_backup_coverage_domain_fresh{host="110"} == 0
expr: awoooi_backup_coverage_domain_fresh == 0
for: 15m
labels:
severity: critical
layer: host-backup
component: backup-coverage
host: "110"
team: ops
alert_category: infrastructure
notification_type: TYPE-3

View File

@@ -191,6 +191,24 @@ def _newest_file_timestamp(patterns: list[str]) -> int:
return newest
def _newest_tree_timestamp(root: Path, max_entries: int = 5000) -> tuple[int, int]:
if not root.exists():
return 0, 0
newest = 0
count = 0
for path in root.rglob("*"):
try:
if not path.is_file():
continue
count += 1
newest = max(newest, int(path.stat().st_mtime))
except OSError:
continue
if count >= max_entries:
break
return newest, count
def _read_backup_110_timestamp() -> int:
candidates = [
Path("/home/ollama/node_exporter_textfiles/backup.prom"),
@@ -907,6 +925,30 @@ def _collect_188(host: str) -> list[str]:
sample_count=1,
)
)
gitea_mirror_ts, gitea_mirror_count = _newest_tree_timestamp(Path("/home/ollama/backup/110/gitea"))
gitea_mirror_fresh = 1 if gitea_mirror_ts and int(time.time()) - gitea_mirror_ts <= 25 * 3600 else 0
lines.extend(
_metric_lines_for_job(
host=host,
job="gitea_repo_mirror_from_110",
source="188-rsync-subtree",
target="/home/ollama/backup/110/gitea",
backup_type="rsync_subtree",
last_success=gitea_mirror_ts,
max_age_hours=25,
sample_count=gitea_mirror_count,
)
)
coverage_labels = (
f'host="{_escape_label(host)}",'
'domain="service",'
'required_jobs="backup_from_110,gitea_repo_mirror_from_110"'
)
lines.append(f"awoooi_backup_coverage_domain_expected_info{{{coverage_labels}}} 1")
lines.append(
"awoooi_backup_coverage_domain_fresh"
f"{{{coverage_labels}}} {1 if gitea_mirror_fresh else 0}"
)
momo_ts = _newest_file_timestamp([
"/home/ollama/momo_backups/*.sql.gz",
"/home/ollama/momo-pro/backups/*.sql.gz",

View File

@@ -49,5 +49,15 @@ def test_backup_exporter_emits_domain_level_backup_coverage() -> None:
assert "awoooi_backup_coverage_domain_expected_info" in exporter
assert "awoooi_backup_coverage_domain_fresh" in exporter
assert "gitea_repo_mirror_from_110" in exporter
assert "/home/ollama/backup/110/gitea" in exporter
for domain in ["host", "database", "website", "service", "package", "tool", "log"]:
assert f'"{domain}"' in exporter
def test_backup_alerts_cover_188_gitea_mirror_subtree() -> None:
alerts = read("ops/monitoring/alerts-unified.yml")
assert 'awoooi_backup_coverage_domain_expected_info{host="188"}' in alerts
assert "BackupCoverageDomainMetricMissing188" in alerts
assert "awoooi_backup_coverage_domain_fresh == 0" in alerts