FR-167 · 技術規格 · 2026-11-01

主機升級程式

guidantai upgrade/recover/更新單:第 0~8 步、時限、資料庫複本、退回、進度檔、systemd、目錄佈局

主系統主機上真正換版的是 guidantai upgrade。它先換成新版的自己,再跑八步:預檢、停接新工作、複製資料庫、在複本上升級、健康檢查、換接、清理,失敗時跑第 8 步退回。現在那份資料庫從頭到尾不被寫入:升級做在複本上,換接前任何一步失敗就丟掉複本、起回舊版。本頁每個數字都對到程式碼行號,路徑以 scripts/installer/ 為根。

真相來源:scripts/installer/lib/*.sh 第 3~6 步合計上限 3600 秒 換接寫完 succeeded 後不退回
§1

範圍

這份寫的是主系統主機側的升級程式:guidantai、install.sh --upgrade、lib/ 底下的步驟檔、systemd 單元、進度檔。主系統後端怎麼寫更新單、怎麼讀進度檔,寫在 state-machines-spec.md 與 sequence-diagrams-spec.md。Agent 主機的升級程式形狀相同,差異在 §11。

讀者要找的 看哪一節
這台機器上有哪些指令、退出碼是什麼 §2 入口與子命令
某一步卡住、逾時是怎麼判的 §4 八步總表、§5 各步細節
資料庫放在哪、換接後舊的在哪 §6 資料庫複本與換接
升級中斷(斷電、kill)之後會怎樣 §7 退回路徑
前端、維護頁讀到的 JSON 長怎樣 §8 進度檔
主系統寫的更新單長怎樣、主機怎麼驗 §9 更新單
主機上檔案放在哪 §10 systemd 單元、§12 目錄佈局
§2

入口與子命令

子命令

指令 誰呼叫 做什麼 程式位置
sudo guidantai upgrade <guidant-ai-<版>.tar.gz> [--deadline <時間>] [--force] IT 手動 驗包、換自己、跑八步。包旁要有 .sha256 與 .release.json guidantai:1487-1489 → lib/upgrade_runner.sh
sudo guidantai recover IT 手動 上次升級停在第 3 步之後沒收尾、或自動退回失敗時,手動退回 guidantai:1490 → upgrade_rollback.sh --recover
guidantai upgrade --from-order guidant-upgrade-order.service 讀 upgrade/orders/ 的更新單 upgrade_runner.sh:241 → upgrade_order.sh
guidantai upgrade --boot-check guidant-upgrade-boot-check.service 開機時看上次升級有沒有跑完 upgrade_runner.sh:242 → upgrade_boot_check.sh
sudo guidantai diag [--since][--request-id][--out][--no-limit] IT 手動或 diag 更新單 產診斷包,含升級進度檔與更新單 guidantai:656、lib/diag_upgrade_stage.sh
guidantai --version 任何人 印 /usr/local/lib/guidant/VERSION(維運指令本身的版本) guidantai:1485-1486
sudo ./install.sh --upgrade [--force] IT 在解開的包目錄 以 --bundle-dir 交給同一支 runner,第 0 步只裝不驗章 install.sh:2606-2621
install.sh --upgrade-phase <config|post-migrate|post-switch|ops-cli> 步驟檔內部 設定補齊的四段,每段冪等 install.sh:2563-2604

upgrade-host 不是主系統的子命令,是 Agent 安裝程式的子命令(upgrade-host boot-check|order),見 §11。

runner 的參數

參數 意思 位置
<tar.gz> 出貨包。第 0 步只抽維運指令,整包由第 4 步解到 upgrade/bundles/ upgrade_runner.sh:193-194
--bundle-dir <目錄> 已解開的包(install.sh --upgrade 用),不驗包簽章 :187
--deadline <時間> 停接新工作的等待截止(date -d 看得懂的格式) :188、:214-216
--force 新舊版號相同也照跑(同版號重編映像) :190
--root <r> 只給測試:維運指令裝到 <r>/usr/local :189

出貨包與 --bundle-dir 只能擇一(:198)。

退出碼

以 runner 檔頭為準(upgrade_runner.sh:13-14):

碼 意思 系統狀態
0 升級成功(含「換接後才逾時」,見 §5.6) 新版服務中
1 失敗,什麼都沒動(第 0~2 步失敗、已有升級在跑) 舊版照常
2 參數錯誤、資料目錄不合理 沒開始
3 失敗,已自動退回 舊版服務中
4 退回也失敗,要人處理 sudo guidantai recover 不確定,看進度檔
10 這次不更新(停接新工作在時段內等不到,或更新單被取消) 舊版照常

陷阱:guidantai --help 的退出碼表與 runner 不同

guidantai:185-194 的說明寫「1=參數或用法錯誤」,那是維運指令其他子命令的意思;upgrade 子命令是 exec runner,回的是上表。寫自動化判退出碼時照上表。

步驟檔的執行契約

每步以子行程 bash <步驟檔> 執行(upgrade_runner.sh:16-18),環境變數帶:

變數 內容
UPGRADE_DIR <資料目錄>/upgrade
GUIDANT_DATA_DIR 資料目錄(/etc/guidant-ai/install.conf 定位)
UPG_JOB_ID 這次升級的編號,形狀 YYYYmmdd-HHMM-<hex>(:253)
UPG_FROM/UPG_TO 現行版(讀 .env 的 GUIDANT_VERSION)/目標版(讀清單 version 或包內 VERSION)
UPG_TARBALL/UPG_BUNDLE_DIR 出貨包路徑/解開後的目錄(給 tar.gz 時是 upgrade/bundles/guidant-ai-<版>)
UPG_STEP_N 第幾步
UPG_DEADLINE 截止時間 epoch 秒(空=沒有)
UPG_FORCE --force 時為 1

步驟回 0=過、回 10(UPG_RC_SKIP,progress.sh:31)=這次不更新、其他=失敗;逾時由 runner 判成 124(timeout -k 收掉的 137 也算 124,upgrade_runner.sh:80-82)。

§3

第 0 步:先換成新版的自己

舊版主機上的 guidantai 只負責第 0 步:驗包、把包內新版維運指令裝上去,然後 exec 新版跑第 1 步之後。所以第 1 步以後跑的永遠是新版的步驟檔,修 runner 的錯不必等客戶先升一次維運指令。

項目 內容 位置
何時跳過 給的是 tar.gz、/usr/local/lib/guidant/VERSION 已等於目標版、沒設 UPG_FORCE_SELF_REPLACE=1:記 skipped「維運指令已是 X,不重裝」 upgrade_runner.sh:261-265
來源優先序 ① 包旁小檔 guidant-ai-<版>.ops.tar.gz:驗過章的清單有 ops_sha256 且雜湊對得上才用 ② 否則從整包抽(要讀完整包) self_replace.sh:70-87、:186-193
時限 小檔可用=60 秒;從整包抽=每 GB 60 秒、下限 300;--bundle-dir=60 秒 upgrade_runner.sh:56-68
驗包順序(整包) 清單簽章(用本機已裝的印樣)→ 整包 sha256=.sha256 檔=清單 sha256 → 版號格式 self_replace.sh:44-66
抽哪些檔 VERSION、guidantai、guidantai_bugsink.sh、lib/、release-keys/、maintenance/,只跑一次 tar;包內 VERSION 要等於清單版號 self_replace.sh:93-105
安裝 先複製到同檔案系統暫存目錄,舊 lib/ 改名讓位、新的 mv 進去;失敗換回舊版。換檔一律 rename,不原地覆寫 self_replace.sh:140-183
順便裝 發布印樣(release_keys_install)、三個 systemd 單元(systemd_units_install)、維護頁三支到 upgrade/maintenance/ self_replace.sh:179-182
交棒 exec env GUIDANT_UPGRADE_REEXEC=1 /usr/local/bin/guidantai upgrade <原參數>;新版看到旗標就不重鎖、不重跑第 0 步,並核對進度檔的 job_id 與 UPG_JOB_ID 相同 upgrade_runner.sh:247-251、:272
失敗 頂層記 skipped、result=unchanged,退出碼 1 upgrade_runner.sh:112-117

陷阱:換檔不可原地覆寫

舊版 guidantai 與 runner 此刻正被 bash 逐行讀取。覆寫同一個 inode,舊行程會讀到新檔的後半段;rename 後舊 inode 留到行程結束才釋放(self_replace.sh:21-22)。

連帶:改步驟順序或時限時,要想到舊版 runner 只會跑到第 0 步為止(upgrade_runner.sh:25-26)。

陷阱:開機檢查單元要在第 3 步之前就裝好

第 0 步就裝 systemd 單元,是因為第 3 步停服務之後斷電,要有人在開機時負責退回(systemd_units.sh:32-33)。

§4

八步總表

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  S0["0 self_replace<br/>換成新版維運指令"] --> S1["1 preflight<br/>預檢"]
  S1 --> S2["2 drain<br/>停接新工作"]
  S2 --> S3["3 db_copy<br/>停服務、複製資料庫"]
  S3 --> S4["4 apply_on_copy<br/>在複本上升級"]
  S4 --> S5["5 health<br/>健康檢查"]
  S5 --> S6["6 switch<br/>換接"]
  S6 --> S7["7 cleanup<br/>清理"]
  S0 -. 失敗 .-> U["skipped/unchanged<br/>退出碼 1"]
  S1 -. 失敗 .-> U
  S2 -. 失敗 .-> U
  S2 -. 時段到 .-> K["skipped/unchanged<br/>退出碼 10"]
  S3 -. 失敗或逾時 .-> R["8 rollback<br/>退回"]
  S4 -. 失敗或逾時 .-> R
  S5 -. 失敗或逾時 .-> R
  S6 -. 寫 succeeded 前失敗 .-> R
  R --> RB["rolled_back 退出碼 3<br/>或 rollback_failed 退出碼 4"]
  S7 -. 失敗 .-> W["只記 warning<br/>結果仍是 upgraded"]
圖 1 — runner 的步驟鏈:第 0~2 步失敗什麼都沒動,第 3~6 步失敗走第 8 步退回

時限常數全在 upgrade_runner.sh:36-47,環境變數覆寫只給測試縮時用。

步 name 步驟檔 做什麼(一句) 時限 失敗後
0 self_replace self_replace.sh 驗包、換成新版維運指令、exec 新版 60 秒,或每 GB 60 秒下限 300 skipped/unchanged,退出 1
1 preflight upgrade_preflight.sh 只讀檢查,不過的項目一次列完 120 秒(UPG_LIMIT_PREFLIGHT) skipped/unchanged,退出 1
2 drain upgrade_drain.sh 寫 draining 旗標,等進行中的背景工作與 Agent 任務做完 沒有自己的時限;有 --deadline 就到截止為止 時段到:skipped,退出 10;其他失敗:skipped,退出 1
3 db_copy upgrade_db_copy.sh 停服務、留設定快照、cp -a 資料庫到 pgdata-next 有預估=estimate_copy_s×3+300;沒預估=1800;再受總上限 退回
4 apply_on_copy upgrade_apply_on_copy.sh 解包、載映像驗雜湊、設定補齊、以複本起 db、migrate 只受總上限;載映像 300 秒、單支 migration 600 秒無進度由步驟自己判 退回
5 health upgrade_health.sh 以複本起受檢 api,等健康、實打版本/登入/三支 GET 300 秒(UPG_LIMIT_HEALTH),再受總上限 退回
6 switch upgrade_switch.sh 複本轉正、寫版號、起新版、寫 succeeded、最後重建前門 300 秒(UPG_LIMIT_SWITCH),再受總上限 寫 succeeded 前退回;之後不退回
7 cleanup upgrade_cleanup.sh 只留本版與上一版的映像、上一份快照、本次的包 600 秒(UPG_LIMIT_CLEANUP) 只記 warning
8 rollback upgrade_rollback.sh 停新版、放回原份、換回設定、起舊映像、健康檢查 900 秒(UPG_LIMIT_ROLLBACK) rollback_failed,退出 4

總上限:第 3~6 步合計 3600 秒(UPG_TOTAL_LIMIT),從第 3 步開始計時(upgrade_runner.sh:150)。每步實際時限=「該步自己的時限」與「總上限剩餘」取小者,該步沒有自己的時限就用剩餘(:99-105)。

逾時怎麼收:timeout -k 30 <秒>,先送 TERM、再等 30 秒(UPG_KILL_GRACE)送 KILL(:47、:80)。步驟檔都有 TERM trap,收到就清自己的半成品。

失敗原因怎麼留:步驟自己寫的 detail 會保留,runner 只在後面附「(退出碼 N)」(:91-92)。維護頁與更新中心顯示的就是這段。

§5

各步細節

5.1 第 1 步 預檢

upgrade_preflight.sh:285-306。這一步什麼都不能動:不刪映像、不清快取、不停服務(:7)。每一項都跑完才下結論,不過的項目一次列出(:8)。

檢查 不過時的字樣(節錄) 位置
既有安裝:.env、guidant.env 在、讀得出現行版號 「找不到 …/.env,這台主機看起來還沒安裝過」 :42-46
出貨包:主機有 python3(或 perl 的 JSON::PP 等三模組)、驗章、雜湊;--bundle-dir 時看 VERSION 「出貨包驗證失敗:…」 :110-133
版本:目標比現行新(--force 可同版)、不降版、現行 ≥ 清單 min_base_version 「X 只能從 Y 以上直升,現行是 Z。必經路徑:…」 :70-107
docker 可用、資料庫連得上 「資料庫連不上:guidant-db 未在執行或帳號密碼不符」 :150-162
核心:/var/run/reboot-required 存在、或執行中核心比已裝最新的舊;實際以 --security-opt no-new-privileges 起一次現行 be 映像(60 秒) 「系統已安裝新核心但尚未重開機,請先 sudo reboot 再升級」 upgrade_preflight_host.sh:41-58
空間:同一檔案系統的需求加總+2GB 保留,各自比可用 「<掛載點> 空間不足:可用 A,需要 B(…保留 2.0GB),還差 C」 :194-244

空間怎麼算(:194-215):

項目 大小 放在哪
包解開 出貨包大小 upgrade/
新映像 出貨包大小(映像層本來就壓過) docker 根目錄
資料庫複本 pgdata 大小 × 120%(PF_DB_COPY_FACTOR_PCT) 資料目錄下(pgdata-next 的上層)
保留 每個檔案系統 2GB(PF_RESERVE_KB) —

停機預估(:259-272):在複本落點寫 100MB(dd … conv=fsync)量速率,estimate_copy_s=資料庫大小÷速率,estimate_downtime_s=estimate_copy_s×3。量不到就不寫,第 3 步用預設 1800 秒。

寫進度檔:步驟 detail(每項一行)、步驟 failures(不過項目的字串陣列)、頂層 estimate_copy_s/estimate_downtime_s(:297-305)。

陷阱:給人看的不過項目讀 failures,不要讀 detail

步驟失敗時 runner 會在 detail 後面附退出碼(upgrade_preflight.sh:12)。畫面要逐項列,用 failures 陣列。

5.2 第 2 步 停接新工作

upgrade_drain.sh。

項目 值 位置
旗標檔 upgrade/draining,內容 job_id=… 與 since=…,寫暫存再 mv :60-65
「進行中」的定義 public.background_jobs 的 running + compliance.agent_tasks 的 dispatched/running。queued/pending 不算:旗標一放主系統就不再撿 :12-13、:93-104
查詢帳號 .env 的 GUIDANT_DB_ADMIN_USER(預設 cmmgr),並 SET app.is_super_admin='t' :76-88、:100
查詢間隔 10 秒(UPG_DRAIN_INTERVAL) :27
看到 0 之後 隔 3 秒再查一次仍是 0 才算過(UPG_DRAIN_SETTLE) :31、:121-124
等太久的提醒 1800 秒還沒完,寫步驟 notice :26、:131-135
放棄 截止前 5 秒自己回 10(UPG_DRAIN_DEADLINE_MARGIN),不讓 runner 的 timeout 收成 124 :29、:136
查不到資料庫 連續 30 次就判失敗(UPG_DRAIN_MAX_DB_FAIL),不把「查不到」當 0 :33、:127
寫進度檔 步驟 running_jobs、running_agent_tasks(數字)、progress 白話 :106-110

陷阱:旗標只在回 0 時留著

放棄、失敗、被 TERM 都要刪旗標,否則主系統一直不接單、畫面看不出原因(upgrade_drain.sh:15-16)。旗標由第 6 步在新版 api 健康後、或第 8 步在舊版起來後才刪。

陷阱:不可用 cm_app 查

cm_app 受 RLS 擋,沒帶租戶脈絡時 count 永遠是 0,會被當成「做完了」直接往下停服務(upgrade_drain.sh:17-18)。

5.3 第 3 步 停服務、複製資料庫

upgrade_db_copy.sh:214-226。

順序 動作 位置
① 確認來源:upg_pgdata_dir(.env 有 GUIDANT_PGDATA_DIR 就用它,沒有=named volume guidant_guidant-pgdata 的 mountpoint),要有 PG_VERSION;來源若就是複本位置=上次沒收尾,要先 recover :148-157
② 停 guidant-api、guidant-worker、guidant-socketio,再以 stop -t 120 停 guidant-db;任一還在跑就不複製。前門 guidant-fe 不停 :126-137
③ 設定與憑證快照到 <資料目錄>/.upgrade-config-prev/(700),項目見下表;MANIFEST 記每一項存在或不存在,JOB_ID 記這次編號 :56-73
④ cp -a pgdata pgdata-next.partial(背景跑、每 10 秒寫百分比),完成後比檔案數、對得上才 mv 成 pgdata-next;進度檔 db 改成 {"current":"pgdata","copy":"pgdata-next"} :164-190

設定快照項目(DBC_CONFIG_ITEMS,:31):.env、guidant.env、docker-compose.yml、certs、pki、seaweedfs/s3.json、ai-gateway-rules。

失敗或被 TERM(:194-212):先起回舊版服務(成功就刪 draining),再刪複本與這次的設定快照,db 改回 copy:null。別次升級留下的快照不動。

證據檔不複製:物件儲存(SeaweedFS)不在複本範圍(:7-8)。

陷阱:「pgdata-next 存在」等於「複本完整」

複製一律先寫 .partial,完整且核對過才改名(upgrade_db_copy.sh:12)。後面每一步都靠這個約定判斷,不要直接寫 pgdata-next。

5.4 第 4 步 在複本上升級

upgrade_apply_on_copy.sh:186-201。

順序 動作 時限或判定 位置
① 給 tar.gz 時解整包到 upgrade/bundles/.extract-<名>,核對 VERSION=目標版才改名成 bundles/guidant-ai-<版> 只受總上限 :46-62
② 載映像:從新包 install.sh 取出 load_images,逐檔比對 bundle-manifest.sha256,不符就拒 300 秒(UPG_LOAD_LIMIT) :67-93
③ install.sh --upgrade-phase config:分類器變數、Redis 帳號、物件儲存、Agent PKI、AI 閘道規則、Redis 密鑰、AI 金鑰加密鑰、儲存設定 — install.sh:2577-2586
④ 以 GUIDANT_PGDATA_DIR=pgdata-next 起 guidant-db,等 healthy,再用 docker inspect 確認掛的真的是複本 180 秒(UPG_DB_READY_LIMIT) :100-117
⑤ GUIDANT_INIT_MODE=migrate MIGRATE_APPLY=1 跑 init 映像(容器名 guidant-upgrade-migrate),輸出寫 <資料目錄>/log/upgrade-migrate-<job_id>.log,每 2 秒讀新行 單支 600 秒沒有新的 PROGRESS 行就判卡住(UPG_STALL_TIMEOUT) :144-173
⑥ install.sh --upgrade-phase post-migrate:錯誤追蹤站、AI 金鑰、儲存快照補建、內建儲存 TLS、弱點資料 — install.sh:2587-2594

進度怎麼來:init 映像的 migrate.sh 每套一支印一行 PROGRESS<TAB>n/總數<TAB>檔名,這裡轉寫成步驟 progress「migration n/總數 檔名」(:126-141)。

陷阱:migrate 前一定先確認 db 掛的是複本

掛錯就是直接改客戶現在那份資料,退回救不回來(upgrade_apply_on_copy.sh:13)。第 5 步會再確認一次(upgrade_health.sh:41-49)。

連帶:移掉 PROGRESS 那行輸出,「單支 10 分無進度」就判不到,卡住要等總上限 60 分(:11-12)。

陷阱:migration log 不放 upgrade/

前門維護頁唯讀掛整個 upgrade/,migration 輸出不該對未登入的人可見(upgrade_apply_on_copy.sh:145)。

5.5 第 5 步 健康檢查

upgrade_health.sh:129-149。

順序 動作 判定 位置
① 再確認 guidant-db 掛的是複本 不是就失敗 :41-49
② up -d guidant-db guidant-redis guidant-seaweedfs,再 compose run -d --no-deps --name guidant-upgrade-api guidant-api 容器不在就失敗 :51-59
③ 等受檢容器 healthy,每 30 秒寫一次進度 180 秒(UPG_HEALTH_READY_LIMIT);容器結束立刻失敗 :62-76
④ 在容器裡打自己 GET /api/1.0/version 200 且 version=目標版 :93-103
⑤ POST /api/1.0/login,帳號是隨機的不存在帳號 401 且回應含 LOGIN_401001 :105-113
⑥ 不帶 token 打 /api/1.0/users/menu、/api/1.0/module-frames/menu、/api/1.0/system/product-update/status 一律 401;404=那組功能沒進新版 :33、:115-124

每次 curl 15 秒逾時(UPG_HEALTH_CURL_TIMEOUT)。受檢容器用完一律刪(:127)。worker 與 socketio 不在這一步起:這時候起 worker 會在還沒驗過的複本上撿單(:29-31)。

陷阱:受檢 api 不可叫 guidant-api

前門靠 docker DNS 名稱 guidant-api 找後端。受檢容器若叫這個名字,前門會立刻把使用者導進還沒驗過的版本、維護頁提早消失;之後檢查不過要退回時,已經有人在新版寫過資料(upgrade_health.sh:7-10)。

5.6 第 6 步 換接

upgrade_switch.sh:120-154。

順序 動作 判定 位置
① 停 guidant-api、guidant-worker、guidant-socketio、guidant-db(stop -t 120) 停不掉就不換接 :53-57
② 換資料夾(§6) 任一 mv 失敗就放回並還原 .env :60-89
③ .env 寫 GUIDANT_VERSION=<新版> — :126
④ 起 guidant-db、guidant-redis、guidant-seaweedfs,等 db healthy 180 秒(UPG_SWITCH_READY_LIMIT) :129-132
⑤ --upgrade-phase post-switch(補灌系統檔)、--upgrade-phase ops-cli(維運指令與部署定義檔換成本版、寫安裝紀錄) 失敗就換接失敗 :133-136
⑥ 起 guidant-api、guidant-sandbox、guidant-socketio、guidant-worker(有 guidant-bugsink 容器才帶),等 api healthy 180 秒 :137-140
⑦ 刪 draining 旗標 — :142
⑧ 進度檔寫 succeeded/upgraded,寫 upgrade/last-switch.json({job_id,to,at}) — :145-146、:111-118
⑨ 重建 guidant-fe,等 healthy 60 秒(UPG_SWITCH_FE_LIMIT);沒就緒只記 detail,不退回 :147-153

換接後才逾時:第 6 步若在寫完 succeeded 之後才逾時(前門重建慢),runner 看到 result=upgraded 就不退回、也不改寫結果,退出 0(upgrade_runner.sh:119-123)。

陷阱:前門一定最後才重建

它重建那幾秒是整個升級唯一看不到維護頁的空檔;在它之前進度檔要已經是 succeeded,維護頁才會顯示「已完成」並自己重新整理(upgrade_switch.sh:15-16)。

陷阱:系統檔與維運指令要在 api 起來之前做完

舊前門一看到新 api 就會把人導過去;這兩段若排在後面,舊版畫面會對著新版 api 跑上一兩分鐘(upgrade_switch.sh:127-128)。

5.7 第 7 步 清理

upgrade_cleanup.sh:114-126。任何一項刪不掉只記 warning,結果仍是 upgraded(upgrade_runner.sh:173)。

項目 留什麼 刪什麼 位置
映像 guidant-ai-be、guidant-ai-fe、guidant-ai-init 各留 UPG_TO 與 UPG_FROM 兩個標籤 其他標籤(latest、<none> 不碰) :37-48
資料庫快照 進度檔 db.prev 指的那一份 其他 pgdata-prev-*;prev_kind=dir 時第一次升級留下的 named volume 也刪(是現行那份就不刪) :65-84
更新包 incoming/ 本版的 .tar.gz、.ops.tar.gz、.release.json(含 .sha256);bundles/guidant-ai-<本版>/agent/ 更早版本的包;本版解開目錄裡 agent/ 以外的大檔 :89-112
更新單 — orders/<job_id>.json、.cancel :108-111

bundles/<本版>/agent/ 要留著:主系統升完後 Agent 跟進、推送、下載都從這裡找 Agent 包(:86-88)。

§6

資料庫複本與換接

6.1 三個路徑函式

都在 lib/upgrade_common.sh,所有步驟只走這三支,不自己拼路徑。

函式 回什麼 位置
upg_pgdata_dir 現在那份:.env 有 GUIDANT_PGDATA_DIR 就用它(相對路徑以資料目錄為準),沒有=named volume guidant_guidant-pgdata 的 mountpoint :31-39
upg_pgdata_copy_dir 複本:<資料目錄>/pgdata-next :43
upg_pgdata_main_dir 換接後的固定位置:<資料目錄>/pgdata :46

compose 檔的 db 掛載是 ${GUIDANT_PGDATA_DIR:-guidant-pgdata}:/var/lib/postgresql/data(docker/production/docker-compose.yml:302)。

6.2 兩種情況(prev_kind)

第一次升級(prev_kind=volume) 之後的升級(prev_kind=dir)
換接前現在那份 docker named volume guidant_guidant-pgdata(.env 沒有 GUIDANT_PGDATA_DIR) <資料目錄>/pgdata
換接時對它做什麼 原封不動(named volume 改不了名);若 <資料目錄>/pgdata 已存在就拒絕換接 mv pgdata pgdata-prev-<舊版>;同名已在(--force 重跑)加 -<YYYYmmddHHMMSS>
複本變成 mv pgdata-next pgdata mv pgdata-next pgdata
.env 寫 GUIDANT_PGDATA_DIR=<資料目錄>/pgdata 同左(值不變)
進度檔 db {"current":"pgdata","copy":null,"prev":"guidant_guidant-pgdata","prev_kind":"volume"} {"current":"pgdata","copy":null,"prev":"pgdata-prev-<舊版>","prev_kind":"dir"}
清理時 下一次升級成功(prev_kind=dir)才刪這個 volume 只留 db.prev 那一份

位置:upgrade_switch.sh:7-11、:38-49、:60-89;清理 upgrade_cleanup.sh:65-84。

陷阱:.env 一定先寫、再搬資料夾

順序反過來時,搬完還沒寫就斷電,下次開機 compose 依舊值接回 named volume,複本裡套好的 migration 等於不存在(upgrade_switch.sh:13-14)。寫 .env 前先留 .env.upgswitch,任何 mv 失敗連 .env 一起放回(:75-85)。

陷阱:快照只給災難救援,不是回滾

換接後 pgdata-prev-<舊版> 是升級前那一刻的資料。新版開放使用後的寫入不在裡面,拿它回去等於丟資料。recover 遇到 result=upgraded 一律不退回(upgrade_rollback.sh:233-235)。

6.3 設定檔怎麼改

upg_set_kv(upgrade_common.sh:71-79)改一個鍵:整份寫暫存再 rename,值經環境變數交給 awk、不上命令列。.env 寫一半會讓 compose 整份解析失敗、服務全起不來。

步驟起新版服務一律用 upg_new_dc_argv(:52-55):env GUIDANT_VERSION=<新版> GUIDANT_PGDATA_DIR=<複本或 pgdata> docker compose --env-file <.env> -f <新包的 compose 檔>。shell 環境優先於 --env-file,所以換接前 .env 還是舊版號也起得出新版。

§7

退回路徑

7.1 三個入口,同一支退回

upgrade_rollback.sh:3-7、:329-340。

入口 何時 鎖 收尾
--from-runner runner 第 3~6 步失敗(第 8 步) runner 持有 runner 寫 rolled_back 或 rollback_failed
--boot guidant-upgrade-boot-check.service 開機時 自己取,拿不到就不動 自己寫;開機時一律回 0
--recover IT 執行 sudo guidantai recover 自己取,拿不到就不動 自己寫;另外先 docker compose -p guidant down --remove-orphans 清 docker 當掉留下的殘骸(不帶 -v)

開機與 recover 先判斷該不該退(rb_decide,:267-280):

進度檔 result 最後一步 開機 recover
沒有進度檔 — 不動 不動
upgraded — 不動 不退回,回 1,提示 guidantai start
unchanged/rolled_back — 不動 不動
rollback_failed — 不動(要人看過再 recover) 退回
null(沒跑完) ≥3 退回 退回
null(沒跑完) <3 只清旗標,寫 failed/unchanged 同左

「沒跑完」看 result 不看 status:runner 失敗時先把 status 改成 failed 才開始退回,退回中被打斷就是 status=failed、result=null(:264-266)。

7.2 退回做什麼

rb_rollback(:232-250),每段都先看「現在是什麼樣子」再動,可以重跑。

順序 動作 位置
① result=upgraded 就拒絕(換接已完成、新版已開放使用) :233-235
② 刪受檢容器 guidant-upgrade-api、guidant-upgrade-migrate;依序停 guidant-api、guidant-worker、guidant-socketio、guidant-sandbox、guidant-bugsink、guidant-db,前門不停 :37-40、:65-85
③ 換接開始過(第 6 步有紀錄,且 db.copy=pgdata-next 或有 prev_kind):把 pgdata 搬回 pgdata-next、快照改回 pgdata;之後一律刪 pgdata-next(.partial 也刪);db 立刻改回 copy:null :118-148
④ 確認升級前那份在:目錄要有 PG_VERSION,或 volume 存在 :150-160
⑤ 換回設定:這次的設定快照(JOB_ID 相同)全部換回;否則有 .env.upgswitch 就換回 .env :163-171
⑥ 確認舊映像在:進度檔 prev_images 有列就照列的,沒列就用 .env 版號推 guidant-ai-be:<版>、guidant-ai-fe:<版> :176-191
⑦ 以舊版 compose 起 guidant-db…guidant-fe 全部(env -u GUIDANT_PGDATA_DIR -u GUIDANT_VERSION,只看 .env) :53-56、:193-203
⑧ db healthy 且掛的是升級前那份、api healthy 且映像是舊版、fe 60 秒內 healthy(沒就緒只提示) :217-227
⑨ 刪 draining、寫 rolled_back_at、刪這次的設定快照 :243-248

等待上限:db 與 api 各 180 秒(UPG_ROLLBACK_READY_LIMIT),fe 60 秒(UPG_ROLLBACK_FE_LIMIT)。整個第 8 步 900 秒。

陷阱:判斷換接做到哪只認換接自己留的記號

看資料夾在不在猜不得:同版號重跑時舊快照也叫 pgdata-prev-<版>。判斷依據是 .env.upgswitch 與進度檔 db.prev_kind(upgrade_rollback.sh:12-13)。放回資料夾之後要立刻把 db 改成 copy:null,否則重跑會把已放回的原份當成升級後那份搬走(:145-146)。

陷阱:設定目錄要在原目錄裡面覆寫

certs/ 與 pki/ 被 redis、seaweedfs、fe 以 bind mount 掛著且升級全程沒停。換掉目錄 inode,容器內就變空目錄,healthcheck 讀不到 CA 轉 unhealthy,之後任何 compose up 都因依賴不健康起不來,退回也起不來(upgrade_db_copy.sh:75-77)。有 rsync 用 rsync -a --delete,沒有就清內容留目錄再複製。

7.3 開機檢查

upgrade_boot_check.sh:rb_standalone boot,任何結果都 exit 0。oneshot 回非零只會讓 systemd 把單元標 failed,docker 的 restart 原則照樣把容器起來;退回失敗的事實記在進度檔,維護頁與 status 看得到(:7-8)。

§8

進度檔

8.1 位置、寫法、讀者

項目 內容 位置
檔案 <資料目錄>/upgrade/upgrade-progress.json,權限 644 progress.sh:51、:126
唯一寫入者 lib/progress.sh 的函式(runner、步驟、更新單處理、退回都 source 它) :3
寫法 每個欄位一個小檔放 upgrade/progress.d/(700),存已編碼的 JSON 值;任何一欄改了就整份重組、寫 .tmp.<pid> 再 mv。讀者永遠讀到完整 JSON :15-16、:111-128
不用 jq 客戶主機不保證有 :17
防撞 有 flock 就鎖 progress.d/.lock(最多等 10 秒),沒有照寫 :82-89
遮罩 字串寫入前遮掉 PASS/PASSWORD/SECRET/TOKEN/PRIVATE_KEY 開頭鍵的值;控制字元去掉 :60-71
讀者 前門維護頁(未登入可讀)、更新中心(GET /system/product-update/status)、開機檢查、診斷包;主系統讀上限 256KB progress.sh:5、product_update_app_service.py:34-64
欄位規則 只增不改名;讀者不認得的欄位要忽略 progress.sh:5
開新一次 progress_init 會刪掉整個 progress.d/ 與 last-switch.json :147-167

陷阱:前門維護頁不用登入就讀得到這個檔

不可寫入密碼、金鑰、連線字串。遮罩只是最後一道,不是可以放心丟進來的理由(progress.sh:12-13)。

8.2 頂層欄位

輸出順序是下表前段的固定順序,其餘欄位依字母排在後面(progress.sh:96-117)。

欄位 型別 誰寫 內容
job_id 字串 progress_init 這次升級的編號;更新單觸發時=單的 job_id
from/to 字串 progress_init 這一段的起訖版號
path 字串陣列 progress_init 更新單的整條路徑(跨版時兩段以上);手動升級=[to]
segment 物件或 null progress_init 跨版時 {"n":第幾段,"of":共幾段},否則 null
triggered_by 物件 progress_init {"kind":"cli|order|preflight","user":"<login>","mode":"now|scheduled","at":"<ISO>"}
started_at 字串 progress_init ISO 時間(主機時區)
status 字串 runner/退回/更新單 見 state-machines-spec.md §3
db 物件 第 3、6、8 步 {"current":"pgdata","copy":null|"pgdata-next"},換接後多 prev、prev_kind
prev_images 字串陣列 progress_init 給 [] 退回要用的舊映像;空陣列時退回用 .env 版號推
result 字串或 null progress_finish 系統此刻的樣子:null(還在跑)/upgraded/unchanged/rolled_back/rollback_failed
error 字串或 null progress_finish 給人看的失敗原因(白話步驟名,如「第 4 步(在複本上升級)逾時,已退回 1.27.6」)
rolled_back_at 字串或 null runner/退回 退回完成時間
updated_at 字串 每次重組 最後一次寫入時間
estimate_copy_s 數字 第 1 步 預估複製秒數(量得到才有)
estimate_downtime_s 數字 第 1 步 estimate_copy_s×3
steps 陣列 每步 見下表

8.3 步驟欄位

欄位 型別 內容
n 數字 0~8
name 字串 程式代號:self_replace、preflight、drain、db_copy、apply_on_copy、health、switch、cleanup、rollback。讀者靠它對自己的翻譯
status 字串 running/ok/failed/timeout/skipped/warning
started/ended 字串 ISO 時間
limit_s 數字或 null 這一步的時限;0 寫成 null
detail 字串 步驟寫的摘要或失敗原因
progress 字串 進行中的白話進度(如「複製資料庫 42%(3.1GB/7.4GB)」)
last_progress_at 字串 寫 progress 時順手記
failures 字串陣列 只有第 1 步:不過的項目
notice 字串 只有第 2 步:等太久的提醒
running_jobs/running_agent_tasks 數字 只有第 2 步:還在跑的件數

步驟白話名(給 error、detail 用)在 progress.sh:35-48;前門維護頁與更新中心各有自己的對照,改一邊要一起改。

8.4 只讀預檢的進度檔

包就緒後主系統會寫一張 type=preflight 的更新單,主機只跑第 1 步(upgrade_order.sh:240-263)。這時進度檔長得不一樣:

欄位 值
triggered_by.kind preflight
status preflight(不是 running:畫面與維護頁把 running 當升級中)
result passed 或 failed
steps 只有 n=1

拿不到升級鎖(正在升級)就不跑,免得 progress_init 清掉正在升級那次的進度檔(:238-239、:244-248)。

8.5 範例(升級中、第 4 步)

{"job_id":"20261101-2200-a1b2","from":"1.27.6","to":"1.27.7","path":["1.27.7"],"segment":null,
 "triggered_by":{"kind":"order","user":"wang.dm","mode":"scheduled","at":"2026-11-01T21:30:00+08:00"},
 "started_at":"2026-11-01T22:00:04+08:00","status":"running",
 "db":{"current":"pgdata","copy":"pgdata-next"},"prev_images":[],
 "result":null,"error":null,"rolled_back_at":null,"updated_at":"2026-11-01T22:03:12+08:00",
 "estimate_copy_s":41,"estimate_downtime_s":123,
 "steps":[
  {"n":0,"name":"self_replace","status":"ok","started":"…","ended":"…","limit_s":60},
  {"n":1,"name":"preflight","status":"ok","started":"…","ended":"…","limit_s":120,"detail":"…"},
  {"n":2,"name":"drain","status":"ok","started":"…","ended":"…","limit_s":null,"detail":"進行中的工作都已做完","running_agent_tasks":0,"running_jobs":0},
  {"n":3,"name":"db_copy","status":"ok","started":"…","ended":"…","limit_s":423,"detail":"資料庫已複製(7.4GB,38 秒);現在那份不會再被寫入"},
  {"n":4,"name":"apply_on_copy","status":"running","started":"…","limit_s":3138,"progress":"migration 31/47 2026-10-14-fr162-mapping-index.sql","last_progress_at":"…"}]}
§9

更新單(主機讀的那一側)

主系統容器只對 upgrade/orders/ 有寫權(目錄 730 root:1000,容器能建檔、刪自己知道名字的檔,但列不出目錄),更新單寫完就由主機的 path 單元接手(systemd_units.sh:20-30)。

9.1 單的形狀

檔名 upgrade/orders/<job_id>.json,上限 8192 bytes(upgrade_order.sh:32)。主系統寫法見 app/product_update/service/upgrade_order.py:83-95。

欄位 值 主機驗什麼
job_id YYYYmmdd-HHMM-<4~8 hex> 形狀;且要等於檔名(:42、:81)
type upgrade/diag/preflight 只收這三個(:82)
mode now/scheduled 只收這兩個;scheduled 一定要有 not_before(:83、:92)
version 最後一段版號 要等於 path 最後一段(:59-60)
path 版號陣列,最多 8 段 每段版號形狀 X.Y.Z(-後綴)、≤64 字(:43、:56-57)
not_before ISO 時間(要帶時區)或 null 沒時區就拒(:61-67)
requested_by login 不合 [A-Za-z0-9._@-]{0,64} 就改成 unknown(:84)
requested_at ISO 時間 不用

用 python3 -I 解(值裡可能有任何東西,不用 grep),符號連結不收(:47-72)。

陷阱:單上的東西只信三件事

單是主系統容器寫的,容器被打穿時單上什麼都可能寫。主機只信:job_id 與版號的格式、包在本機 incoming/ 且用本機印樣驗得過、版號真的比現行新。單上不接受任何路徑,包的位置一律由版號在 incoming/ 下拼出來(upgrade_order.sh:19-21)。

9.2 主機怎麼處理

ord_handle(upgrade_order.sh:266-284):

type/mode 動作 位置
diag guidantai diag --out upgrade/diag/<job_id>,產出改 644 讓 api 容器讀,刪單 :226-237
preflight 刪單,只跑第 1 步(§8.4) :240-263
upgrade 每段重驗包(簽章、雜湊、版號比前一段新);驗不過寫進度檔 skipped/unchanged「執行前重新驗證更新包未通過:…」並刪單 :99-131
upgrade + scheduled 且時間未到 systemd-run --unit=guidant-upgrade-order-<job_id> --on-calendar=<時間> --timer-property=AccuracySec=1s 排一次性 timer,到點以 --run <單> 回來;單留在 orders/ :133-148
upgrade 到點或立即 刪單,逐段交給 upgrade_runner.sh <incoming/guidant-ai-<版>.tar.gz>,帶 UPG_JOB_ID、UPG_TRIGGER_KIND=order、UPG_ORDER_PATH、UPG_ORDER_SEGMENT :166-224

跨版:每段各跑一次 runner(各自複本、各自健康檢查),job_id 相同;任一段失敗就停,前面跑完的段留著(停在中間版,那是可以正常使用的版本)(:16-17)。每段開跑前都驗,第二段的包壞了第一段就不跑(:97)。

取消:主系統在主機已開跑後按取消,會寫 orders/<job_id>.cancel。主機每秒看一次,最後一步編號 <3 時才收掉 runner(setsid 的整個行程群組送 TERM),進度檔寫 skipped/unchanged「已由管理員取消…」,回 10;第 3 步起忽略(:185-224)。排程到點時單已被刪=管理員取消了,什麼都不做(:270-274)。

path 單元只觸發一次:處理途中新進的單不會再觸發,所以處理完要重掃;排程單會留在 orders/ 等 timer,記下處理過的檔名才不會一直重掃(:299-310)。

§10

systemd 單元

單元檔在包內 scripts/installer/systemd/,安裝時把 @GUIDANT_DATA_DIR@ 換成這台的資料目錄,寫暫存再 mv(systemd_units.sh:48-57)。全新安裝與每次升級第 0 步都會重裝。三個單元都是本機事件觸發,不開埠、不常駐。

單元 類型 觸發 執行 關鍵設定 啟用方式
guidant-upgrade-order.path path PathChanged=<資料目錄>/upgrade/orders,MakeDirectory=yes,DirectoryMode=0730 觸發 guidant-upgrade-order.service WantedBy=paths.target enable --now
guidant-upgrade-order.service oneshot 由 path 單元 /usr/local/bin/guidantai upgrade --from-order After=docker.service、Wants=docker.service、TimeoutStartSec=infinity 不 enable(無 [Install])
guidant-upgrade-boot-check.service oneshot 開機 /usr/local/bin/guidantai upgrade --boot-check After=docker.service network-online.target、TimeoutStartSec=1500 只 enable、不 --now
guidant-upgrade-order-<job_id>.timer 暫時 timer systemd-run --on-calendar /bin/bash <lib>/upgrade_order.sh --run <單> AccuracySec=1s,帶 GUIDANT_DATA_DIR 一次性

陷阱:更新單 service 不設時限、用 Wants 不用 Requires

升級本身由 runner 每步時限與 60 分總上限管,這裡再設一個會在升級跑到一半時把整個行程殺掉;Requires 會讓 docker 重啟時連帶停掉這支,升級在第 3~6 步被腰斬(systemd/guidant-upgrade-order.service 檔頭)。

開機檢查只 enable 不 --now:升級中跑它等於自己退回自己(systemd_units.sh:66)。開機檢查的 1500 秒要涵蓋退回本身 900 秒再加停容器(systemd/guidant-upgrade-boot-check.service 檔頭)。

找不到資料目錄(沒有 GUIDANT_DATA_DIR、也沒有安裝紀錄)就不裝,下次升級補上;不猜預設值,猜錯的話 path 單元會監看一個沒人寫的目錄、且不會報錯(systemd_units.sh:12-13、:38-42)。

§11

Agent 主機的升級程式

Agent 升級程式形狀照主系統(步驟名、時限、進度檔同一份契約),在 evidence-agent repo scripts/install/lib/。這裡只列差異;跟進流程見 sequence-diagrams-spec.md 圖 3。

項目 主系統 Agent
入口 guidantai upgrade Agent install.sh upgrade <包>;系統單元呼叫 upgrade-host boot-check|order(upgrade_order.sh:112-118)
第 0 步 self_replace 無換自己;更新單觸發時第 0 步是 extract(解開更新包,upgrade_order.sh:199-207)
資料庫複本 有(第 3 步) 無;compose 形態第 3 步備份 .env 與 compose 檔到 <升級目錄>/prev/(700)
步驟 1 預檢/2 停接/3 複製/4 套用/5 健康/6 換接/7 清理/8 退回 1 preflight/2 drain/3 apply_on_copy/4 switch/5 health/6 cleanup/8 rollback(upgrade_runner.sh:12-20)
時限(秒) 見 §4 預檢 120、套用 900、切換 1800、健康 600、清理 600、退回 900;第 4~5 步合計 3600(upgrade_runner.sh:33-40)
退回起點 第 3 步 compose 第 3 步、原生第 4 步(_upr_first_live_step,:135)
升級目錄 <資料目錄>/upgrade compose=<安裝目錄>/upgrade;原生=<AGENT_DATA_DIR>/upgrade(:59-66)
更新單目錄 upgrade/orders/(容器寫) upgrade/agent-order/(容器 uid 1000 可寫)+agent-staging/(root 700)
systemd guidant-upgrade-* guidant-agent-upgrade-boot-check.service、guidant-agent-upgrade-order.path/.service,入口是 /usr/local/lib/guidant-agent-upgrade/<install.sh> 副本
形態限制 — 跟進升級只支援 docker compose 形態
§12

主機目錄與檔案

<資料目錄> 由 /etc/guidant-ai/install.conf 的 GUIDANT_DATA_DIR 決定(預設 /srv/guidant-ai)。

路徑 誰寫 權限 用途
/usr/local/bin/guidantai、guidantai_bugsink.sh 第 0 步 755/644 root 維運指令
/usr/local/lib/guidant/ 第 0 步 root lib/*.sh 與 VERSION
/etc/guidant/release-keys/<kid>.pub 第 0 步(release_keys_install) root;容器唯讀掛 發布印樣(Ed25519 公鑰),驗章只用這裡已裝的
/etc/guidant-ai/install.conf 安裝、ops-cli 段 root 安裝紀錄(資料目錄、compose 檔位置)
/etc/systemd/system/guidant-upgrade-* 第 0 步 644 §10
<資料目錄>/.env、guidant.env、docker-compose.yml 安裝、config/ops-cli 段、第 6 步 600/— 設定;.env 換接後多 GUIDANT_PGDATA_DIR
<資料目錄>/pgdata/ 第 6 步 postgres 換接後的資料庫
<資料目錄>/pgdata-next/、pgdata-next.partial/ 第 3 步 postgres 複本(升級中)
<資料目錄>/pgdata-prev-<舊版>[-時間]/ 第 6 步 postgres 升級前快照,只留一份
<資料目錄>/.upgrade-config-prev/ 第 3 步 700 設定快照(MANIFEST、JOB_ID、files/);含密碼,不可放 upgrade/ 底下
<資料目錄>/.env.upgswitch 第 6 步 同 .env 換接中 .env 的暫存
<資料目錄>/log/upgrade-migrate-<job_id>.log 第 4 步 600 migration 輸出
<資料目錄>/log/upgrade-<時間>.log、install-<時間>.log install.sh — 安裝程式落檔紀錄
<資料目錄>/upgrade/ — 755;容器唯讀掛 升級交換區
upgrade/upgrade-progress.json progress.sh 644 進度檔
upgrade/progress.d/ progress.sh 700 進度檔的欄位小檔
upgrade/.upgrade.lock runner、退回、預檢單 — 同一台同時只有一個升級
upgrade/draining 第 2 步 644 停接旗標(主系統只看在不在)
upgrade/last-switch.json 第 6 步 644 {job_id,to,at};下次升級開始才刪
upgrade/orders/ 主系統容器 730 root:1000;容器讀寫掛 更新單、.cancel 取消旗標
upgrade/incoming/ 主系統容器 750 1000:1000;容器讀寫掛 下載與上傳的包:guidant-ai-<版>.tar.gz(.part)、.tar.gz.sha256、.release.json、.ops.tar.gz(.part)
upgrade/bundles/guidant-ai-<版>/ 第 4 步 root 解開的包;清理後只剩 agent/
upgrade/maintenance/ 第 0 步 755/644 新版維護頁三支(平放)
upgrade/diag/<job_id>/ diag 單 755/644 診斷包,給更新中心下載

容器掛載(docker/production/docker-compose.yml:248-257、:523-525):api 與 worker 掛 upgrade:ro,orders 與 incoming 以 rw 疊上去,另掛 /etc/guidant/release-keys:ro;前門 fe 只掛 upgrade:ro。

§13

診斷包

guidantai diag 先在主機蒐料到交換目錄,再交給容器內打包核心產 guidant-diag-<時間>.tar.gz;遮罩由容器內那一套做,主機只複製原檔(lib/diag_upgrade_stage.sh:3-5)。

內容 來源 位置
docker-ps.txt、docker-stats.txt、disk.txt docker ps、docker stats、df -h guidantai:722-727
guidant.env 設定檔原檔(容器內遮罩) guidantai:729-736
upgrade-progress.json 進度檔 diag_upgrade_stage.sh:28-32
upgrade-orders.txt orders/*.json 串接 :34-44
upgrade_log-* log/upgrade-*.log 最新三支 :46
install_log-* log/install-*.log 最新三支 :47
last-switch.json 換接標記 :49-53
docker-<服務>.log 每個服務 docker logs --since <預設 24h> --tail 5000 guidantai:654、:742-752

拿不到的項目照樣寫進索引(unavailable 加原因),不讓整包失敗。從更新中心按「下載診斷包」會寫 type=diag 的更新單,產在 upgrade/diag/<job_id>/。

§14

與前門維護頁的關係

前門 guidant-fe 從頭到尾不停(第 6 步最後才重建)。後端停掉期間它送維護頁,維護頁讀進度檔自己畫進度。

項目 內容 位置(compliance-manager-fe)
何時送維護頁 後端 502/503/504 一律改送 /maintenance.html、回 503 nginx.onprem.conf:126
維護頁從哪來 先找 /guidant/upgrade/maintenance/maintenance.html(第 0 步從新版包放上去),沒有才用映像內建的;js/css 同理 :134-170
進度檔 GET /upgrade-progress.json alias 到 /guidant/upgrade/upgrade-progress.json,Cache-Control: no-store :181-189
輪詢 每 3 秒讀進度檔 public/maintenance/maintenance.js:20-21
何時放人回去 進度檔 status=running 或 result=rollback_failed 就不放;否則打 /api/1.0/version,通了才 reload;成功時帶 ?upgraded=1 :331-358

陷阱:第 5 步時 /api/1.0/version 已經通了,但不能放人

第 5 步的受檢 api 不叫 guidant-api,前門找不到它,所以前門打 version 仍是 502;就算打得到,維護頁也先看進度檔還是 running 就不放(maintenance.js:316-317)。兩道都在,拿掉任一道都會讓人提早進到還沒換接的版本。

§15

升級完補寫系統紀錄

升級期間服務停著,主機的紀錄只在主機檔案裡。主系統每分鐘排程在更新紀錄收成終態後,把進度檔每一步各寫一筆 system_logs,再寫一筆整次結果;每張單只寫一次(ROOT 租戶 system_configs 的 upgrade_steps_logged_job 記著)(app/product_update/service/upgrade_order.py:516-585)。

步 白話名 事件碼
0 換成新版維運指令 6239 PRODUCT_UPDATE_STEP_SELF_REPLACE
1 預檢 6240 PRODUCT_UPDATE_STEP_PREFLIGHT
2 停止接新工作,等進行中的做完 6241 PRODUCT_UPDATE_STEP_DRAIN
3 停服務、複製資料庫 6242 PRODUCT_UPDATE_STEP_DB_COPY
4 在複本上升級 6243 PRODUCT_UPDATE_STEP_APPLY_ON_COPY
5 啟動新版、健康檢查 6244 PRODUCT_UPDATE_STEP_HEALTH
6 開放使用 6245 PRODUCT_UPDATE_STEP_SWITCH
7 清理舊版本 6246 PRODUCT_UPDATE_STEP_CLEANUP
8 自動退回 6247 PRODUCT_UPDATE_STEP_ROLLBACK
— 整次結果(誰排的、從哪版到哪版、成功/退回) 6248 PRODUCT_UPDATE_RESULT

事件碼定義在 common/enum/event_code.py:242-251。跨版升級每段各一份進度檔、job_id 相同,這裡只看得到最後一段的步驟。

§16

相關文件

文件 內容
design.md 設計定案 D1~D31、端到端驗收、拆卡
state-machines-spec.md 更新單、Agent 跟進列、進度檔三個狀態機,與畫面顯示的對照
sequence-diagrams-spec.md 檢查到就緒、按更新到回報、Agent 跟進、發版四張循序圖
handoff/fr167-STATE.md §3 已落地的契約(與 design 不同處)
scripts/installer/lib/ 本頁所有行號的來源