scripts/fleet/apply.py vs. 2018 年代的 Fabric (fabfile.py)¶
Terminology rule (zh-TW pages)
技術名詞首次出現以「中文 (English original)」格式呈現,例:依賴注入
(dependency injection)。不自創翻譯——若無公認譯名直接保留英文
(如 embedding、tokenizer)。代碼、API 名、CLI flag、套件名、檔名一律不翻。
對本倉庫的 scripts/fleet/apply.py 與作者較早期的
RaspPi-Cluster fabfile.py
(Fabric 1.x,約 2018 年)進行並列比較。兩者解決相同形狀的問題——一台協調器 (controller)
將命令分派 (fan out) 至 N 台機器——但兩者的實作之間隔了約 7 年累積的營運痛點 (operational pain)。
保留在此作為「為什麼 fleet-apply 長這個樣子?」的考古紀錄。 不論用哪一個工具都不需先讀這頁。
同樣的概念,兩個年代¶
| 關注點 | RaspPi-Cluster fabfile.py (Fabric 1.x) |
scripts/fleet/apply.py (asyncssh) |
|---|---|---|
| 主機清冊 (inventory) | env.hosts + env.passwords 直接寫死於檔案頂端 |
~/.config/fleet/machines.toml(load_hosts,scripts/fleet/apply.py:101) |
| 平行分派 | @parallel decorator(執行緒池 (thread pool) 包住 paramiko) |
asyncio.TaskGroup + 信號量 (semaphore)(單執行緒協程 (coroutine)) |
| Sudo 密碼注入 | env.passwords[host] = pw;Fabric 預期並餵入提示 |
推送至遠端 ~/.cache/chezmoi-fleet/sudo.pass(0600)+ CHEZMOI_SUDO_PASSWORD_FILE(scripts/fleet/apply.py:240) |
| 遠端命令執行 | sudo() / run() |
conn.create_process(cmd)(scripts/fleet/apply.py:807) |
| ssh_config 別名 (alias) 支援 | 原生(Fabric 透過 OpenSSH 的 shell 子程序) | asyncssh 為該別名讀取 ~/.ssh/config(scripts/fleet/apply.py:550) |
| 任務 (task) 框架 | fab install_dependencies / fab change_hostname |
just fleet-apply / just fleet-apply-file PATH |
如果你只在意「一個進入點,把命令 ssh 到一串機器,收回 exit code」——它們是同一個工具,只是不同年代。
這 7 年買到了什麼¶
1. 並行模型:執行緒 → 協程¶
- Fabric 1.x:
@parallel為每台主機開一條 OS 執行緒(底下是 paramiko)。在 5–20 台主機時表現尚可;幾百台時 FD/堆疊壓力很大。 - scripts/fleet/apply.py:單執行緒
asyncio+asyncssh。可擴展性更佳;信號量 (--max-parallel) 在沒有執行緒開銷的情況下界定並行數。
2. Ctrl+C / 通道斷線時的 process tree 清理¶
fabfile 完全沒做——在協調器上按 Ctrl+C 會在每台 Pi 上留下孤兒 (orphan) apt-get / pip install 程序。
scripts/fleet/apply.py 把清理疊了三層
(scripts/fleet/apply.py:430–486):
set -m,這樣每個被背景化的子程序會擁有自己的 process group。- 在包裝 (wrapper) shell 中設置
trap 'pkill -TERM -P $_cz_pid; kill -TERM $_cz_pid' INT TERM HUP。 - asyncssh 設定
request_pty=True,這樣關閉通道會把 SIGHUP 送達遠端 shell,並觸發 trap。
再加上 --kill-orphans 作為清道夫,處理三層都漏掉的情況。
3. 協調器死掉之後的可觀測性 (observability)¶
- Fabric:stdout 在多台主機間交錯;終端機掛了,整輪也就沒了。
- scripts/fleet/apply.py:
- 每次執行都在遠端產生
~/.cache/chezmoi-fleet/logs/<run_id>.log+.exit哨兵檔。 --tail HOST[:RUN_ID]可重新接上仍在執行中的遠端。--status [--watch N]輪詢「每台主機上的 chezmoi/ansible 是否還活著?」- 本地
logs/fleet-apply/<UTC>/<host>.log鏡射相同的串流。
4. 即時 UI¶
Rich 的 Live 表格會在執行過程中為每台主機呈現狀態 / 已耗時 / rc / 最後一行
(scripts/fleet/apply.py:897)。Fabric 只會印出交錯的 stdout。
5. 漂移 (drift) 分類(chezmoi 專屬)¶
_classify_drift()(scripts/fleet/apply.py:520)
會在 chezmoi 的 stderr 僅 含「could not open a new TTY」這類提示時,
把 rc != 0 從 failed 降級為 drift——意即 --keep-going 略過了遠端被手動修改過的目標。
該主機顯示為黃色 ⚠ 並列出漂移路徑,但不會計入 exit code。詳見
fleet-apply.md → "drift ≠ failed"。
Fabric 沒有等價概念;rc != 0 一律是失敗。
6. Sudo session 模型¶
- Fabric:
env.passwords存在協調器記憶體中;每次sudo()呼叫都重新等待提示並重新餵入密碼。 - scripts/fleet/apply.py:密碼只寫一次到遠端的 0600 檔案,然後由
scripts/lib/sudo_shared.sh的sudo_session_init透過sudo -S -v -p ''採用 (adopt)。同一次chezmoi apply中的 22+ 個 ansible role 全都重複使用快取憑證——每個任務都不必再 expect 一次。詳見 sudo-session.md → 「Non-interactive password injection」。
7. 密碼來源¶
Fabric:只支援程式碼內明文。
scripts/fleet/apply.py 支援四種(scripts/fleet/apply.py:59):
| 類型 | 行為 |
|---|---|
plain |
TOML 中的值(請設好檔案權限) |
prompt |
啟動時對每台主機呼叫一次 getpass() |
bitwarden |
bw get password <item>(需要已解鎖的保險庫 + BW_SESSION) |
none |
不使用 sudo(對應 chezmoi init 的 noRoot=true) |
8. 本地主機 = 同一份程式路徑¶
host.local = true 會略過 SSH,直接以本地子程序執行 chezmoi
(run_one_local,scripts/fleet/apply.py:597),
共享協調器的 PATH、tty 與 sudoers 狀態。日誌格式相同,
status / tail 探測也一樣可用。
Fabric 需要分開的 local() 與 run() 兩條程式路徑。
9. Pager / TTY 強化¶
對 chezmoi 命令使用 --no-pager + PAGER=cat GIT_PAGER=cat + 不分配 PTY
(scripts/fleet/apply.py:293–299)。可防範:
bat在 stdout 不是終端機時因為缺主題而崩潰。less永遠卡住等使用者輸入。- chezmoi 讀自己的
pager設定鍵(會忽略PAGER環境變數——這就是必須加--no-pager的原因)。
這些都是這個機隊 (fleet) 上實際遇過的失敗;fabfile 那個年代用的是普通 apt-get,所以從沒踩到。
10. 分支 / topic-branch 工作流¶
--branch BRANCH [--force-checkout] 會在套用前於遠端原始碼目錄 (source dir) 切出 (check out) 一個功能分支。
Fabric 沒有「每台主機上的 dotfiles 倉庫各自有自己 checkout」這個概念。
Fabric 做得比較好的地方¶
為了公平起見:
- 建置成本:
pip install fabric && fab task——不用 asyncio、不用 Rich、不用 TOML schema。scripts/fleet/apply.py是一份 1200 行、能 uv self-installing 的腳本。 - 命令式 DSL:Fabric 讓你直接內嵌寫
sudo("apt install foo"); put("local.conf", "/etc/foo.conf"); run("systemctl restart foo")。scripts/fleet/apply.py只執行一個命令(chezmoi update/apply/diff),因為每台主機的具體邏輯是委派給 chezmoi + ansible 的。如果你想要臨時即興地 ssh fan-out,Fabric(或它的現代後繼者 Fabric 2.x /pyinfra)仍然是較合適的選擇。 - 抽象層較低 = 學習成本較低:
fab -H host1,host2 -- uname -a可以開箱即用。scripts/fleet/apply.py需要一份 TOML 檔加上 chezmoi 設定。
TL;DR¶
| Fabric 年代 (2018) | scripts/fleet/apply.py(現在) |
|
|---|---|---|
| 解決的問題 | 把命令推送到 N 台主機 | 把命令推送到 N 台主機 |
| 並行性 | 執行緒池 | asyncio 協程 |
| 崩潰復原 | 沒有——終端機沒了,整輪就沒了 | 遠端 logs + 哨兵 + --tail / --status / --kill-orphans |
| Sudo | 每次呼叫都 expect | 每台主機寫入一次檔案 + 共享 session |
| UI | 交錯的 stdout | Rich 即時表格 |
| 失敗語意 | rc != 0 = 失敗 | 當符合 chezmoi-skip pattern 時,rc != 0 會降級為 drift |
| 機密來源 | 程式碼內明文 | plain / prompt / bitwarden / none |
| 本地主機 | 獨立的 local() API |
同一份程式路徑(local = true) |
| 程式碼行數 | ~200 | ~1500 |
DNA 是相同的。新版本是「你讓一個工具累積七年『嗯,這上週又咬到我』修正」之後的樣子。