fix(offline): keep the package payload out of a clone, and say so when it is missing

The bundle pool is ~884 MB and now travels through Git LFS, so a plain clone
materialises all of it. Measured on this repository: 2.7 GB working tree, against
20 MB with smudge disabled -- the same 135x that decides whether one device or
seven hundred can be provisioned from a git remote.

bf-bootstrap exports GIT_LFS_SKIP_SMUDGE=1 for every git operation it performs
(clone and fetch), so provisioning a device no longer implies downloading the
package payload. The trade is explicit and documented rather than hidden: with
smudge off the pool files on disk are 131-byte pointer texts, and the installer
must not mistake one for a package.

So the installer gained a guard. A pointer text satisfies every existence check,
so without it the run died later inside `sha256sum -c` and reported a checksum
mismatch -- a misdiagnosis that sends an operator looking for corruption when the
payload was simply never fetched. It now recognises the pointer by content and
names the remedy instead.

Getting the payload is a deliberate second step, and both documented routes are
real: a local mirror (the offline-correct path, and the one the fleet uses) or
`git lfs pull --include='provisioning/offline-repo/built/pool/*'`, which fetches
only the delivery pool and leaves the intake copy under packages/ unfetched --
verified: 1.8 GB and a real .deb in built/pool, 131-byte stub still in packages/.

Tests: test-field-os-lifecycle.sh gains a scenario that lays down a pointer text
where a .deb belongs and asserts three things -- the run refuses, the message
names the cause, and it does not blame a checksum. Also asserts apt-get is never
invoked. 16 scenarios, all passing.

docs/27 is corrected where it had gone stale: it claimed a clone carries the
packages and the installer downloads nothing, which stopped being true the moment
the payload moved to LFS.
This commit is contained in:
emirhanmamak-blueforce 2026-09-16 22:41:53 +03:00
parent 337db15d5b
commit 06c96690ca
4 changed files with 79 additions and 8 deletions

View file

@ -77,7 +77,25 @@ EOF
say() { printf 'bf-bootstrap: %s\n' "$*"; } say() { printf 'bf-bootstrap: %s\n' "$*"; }
warn() { printf 'bf-bootstrap: WARN: %s\n' "$*" >&2; } warn() { printf 'bf-bootstrap: WARN: %s\n' "$*" >&2; }
die() { printf 'bf-bootstrap: ERROR: %s\n' "$*" >&2; exit 1; } die() { printf 'bf-bootstrap: ERROR: %s\n' "$*" >&2; exit 1; }
# run: executes a mutating command, or only reports it under --check.
# Git-LFS smudge is disabled for every git operation this script performs.
#
# Why: the offline package pool is ~884 MB of .deb payload tracked through Git
# LFS, and a default clone materialises all of it (measured: 2.7 GB working tree
# against 20 MB with smudge off, a 135x difference). A device that only needs to
# reach the console and the manifest pays that cost on every clone for bytes it
# may never install from.
#
# The trade is explicit: with smudge off the pool files on disk are 131-byte
# pointer texts, and the installer refuses such a bundle rather than installing
# from it ("Nokta dosyası" below). Fetching the payload is therefore a separate,
# deliberate step -- `git lfs pull --include='provisioning/offline-repo/built/pool/*'`
# -- so that "I cloned the repository" and "I have the packages" stay two facts.
#
# This is deliberately the smudge-off half only. Enabling a full `git lfs pull`
# here would reintroduce the 884 MB download this prevents.
export GIT_LFS_SKIP_SMUDGE=1
run() { run() {
if [[ "$CHECK_MODE" -eq 1 ]]; then if [[ "$CHECK_MODE" -eq 1 ]]; then
printf 'bf-bootstrap: check: would run: %s\n' "$*" printf 'bf-bootstrap: check: would run: %s\n' "$*"

View file

@ -1,6 +1,6 @@
# 27 — Offline Provisioning # 27 — Offline Provisioning
> Kısa özet: Paketler **git deposunun içindedir** (`provisioning/offline-repo/packages/`, 1321 `.deb`, ~888 MB, Git LFS ile takip edilir). Saha kurulumu internet **gerektirmez** ve kuran kişi **hiçbir şey indirmez**: `sudo bf` → Kurulum sekmesi → "Bu cihazı kur" → 8 haneli bayi no → onay. Bu yol yalnız `PROVISIONED_OFFLINE` durumuna getirir; cihaz, tek kullanımlık enrollment token ve merkezi kanallar doğrulanmadan `ENROLLED` veya `READY` olmaz. > Kısa özet: Paketler **git deposunun içindedir** (`provisioning/offline-repo/built/pool/`, 1321 `.deb`, ~884 MB, Git LFS ile takip edilir). Saha kurulumu internet **gerektirmez**: `sudo bf` → Kurulum sekmesi → "Bu cihazı kur" → 8 haneli bayi no → onay. Paket yükü klonla birlikte gelmez (bilerek; bkz. §4), yerel aynadan taşınır ve kurucu depoyu kendi bulur. Bu yol yalnız `PROVISIONED_OFFLINE` durumuna getirir; cihaz, tek kullanımlık enrollment token ve merkezi kanallar doğrulanmadan `ENROLLED` veya `READY` olmaz.
> >
> - Dosya: `docs/27-OFFLINE-PROVISIONING.md` > - Dosya: `docs/27-OFFLINE-PROVISIONING.md`
> >
@ -13,7 +13,7 @@
## 1. Amaç ## 1. Amaç
Saha interneti kesik veya güvenilmezken tekrarlanabilir, denetlenebilir ve secretsiz ilk kurulumu tanımlamak. Kuran kişi paket aramaz, indirmez, bayrak ezberlemez: paket seti depoyla birlikte gelir. Saha interneti kesik veya güvenilmezken tekrarlanabilir, denetlenebilir ve secretsiz ilk kurulumu tanımlamak. Kuran kişi paket aramaz, indirmez, bayrak ezberlemez: paket seti depoda sürümlenir ve yerel aynadan taşınır.
## 2. Kapsam ## 2. Kapsam
@ -33,7 +33,9 @@ LİSANS: APT — https://manpages.ubuntu.com/manpages/noble/en/man8/apt.8.html
## 4. Neden Bu Karar? ## 4. Neden Bu Karar?
**Paketler artık ayrı bir medyada değil, bu git deposunda taşınır:** `provisioning/offline-repo/packages/` altında 1321 `.deb` (~888 MB) durur ve `.gitattributes` ile **Git LFS** üzerinden takip edilir (`*.deb filter=lfs ...`). Bu yüzden sahada "hangi paket nereden geldi?" sorusu ne internete ne de dış medyaya bağlıdır; aynı soru `packages.lock.tsv` ve `repo-manifest.yaml` ile yanıtlanır. Kuran kişi paket aramaz ve **indirmez**; klonladığı depo seti zaten taşır. **Paketler ayrı bir medyada değil, bu git deposunda taşınır:** `provisioning/offline-repo/built/pool/` altında 1321 `.deb` (~884 MB) durur ve `.gitattributes` ile **Git LFS** üzerinden takip edilir (`*.deb filter=lfs ...`). Bu yüzden sahada "hangi paket nereden geldi?" sorusu ne internete ne de dış medyaya bağlıdır; aynı soru `packages.lock.tsv` ve `repo-manifest.yaml` ile yanıtlanır.
Kuran kişi paket **aramaz**, ama paket yükünü kendiliğinden de indirmez — bu bilinçli bir ayrımdır. LFS yükü ~884 MB olduğu ve 700 cihazın her biri onu çekerse aylık bant kotası ilk günde tükeneceği için klon hafif tutulur; yük **yerel aynadan** (USB ya da repo sorumlusunun bıraktığı kopya) gelir, sahada internet gerekmez. Yalnız internet varken ve tek seferlik bir çekme uygunsa `git lfs pull --include='provisioning/offline-repo/built/pool/*'` kullanılır.
Durum makinesi, "kuruldu" ile "merkezce güvenildi" arasındaki farkı görünür yapar: offline kurulum yalnız `PROVISIONED_OFFLINE` üretir. Durum makinesi, "kuruldu" ile "merkezce güvenildi" arasındaki farkı görünür yapar: offline kurulum yalnız `PROVISIONED_OFFLINE` üretir.
@ -56,7 +58,7 @@ Durum makinesi, "kuruldu" ile "merkezce güvenildi" arasındaki farkı görünü
## 7. Dezavantajlar ## 7. Dezavantajlar
- Git deposu paket setiyle birlikte büyür (LFS clone/pull maliyeti); derin clone sahada zaman alabilir. - Git deposu paket setiyle birlikte büyür (LFS; içerik-hash ile tekilleşir, ~884 MB). Klon hafiftir ama paket yükü ayrıca taşınmalıdır: 700 cihazın her biri LFS'ten çekerse bant kotası tükenir, bu yüzden yük yerel aynadan dağıtılır.
- Offline cihaz merkezi izleme ve erişim kanalları doğrulanana kadar operasyonel olarak hazır değildir. - Offline cihaz merkezi izleme ve erişim kanalları doğrulanana kadar operasyonel olarak hazır değildir.
- Paket snapshot'ı yenilenmediği sürece güvenlik güncellemeleri geride kalır (onaysız update zaten yasak, K-11). - Paket snapshot'ı yenilenmediği sürece güvenlik güncellemeleri geride kalır (onaysız update zaten yasak, K-11).
@ -81,7 +83,16 @@ Durum makinesi, "kuruldu" ile "merkezce güvenildi" arasındaki farkı görünü
### Kuran kişinin akışı (grafik arayüz — ana yol) ### Kuran kişinin akışı (grafik arayüz — ana yol)
1. Depoyu getir: `git clone <repo>` (paketler Git LFS ile gelir) — veya repo sorumlusunun bıraktığı kopyada `/opt/blueforce/offline-repo` hazırdır. 1. Depoyu getir: `git clone <repo>` — **paketler bilerek gelmez.** Bootstrap (ve her git işlemi) `GIT_LFS_SKIP_SMUDGE=1` ile çalışır, çünkü paket yükü ~884 MB'tır ve tam klon sahada 2.7 GB'lık bir çalışma kopyası üretir (ölçüldü; smudge kapalıyken 20 MB). Paket yükünü almanın iki yolu var:
- **Yerel ayna (önerilen, sahada internet yok):** repo sorumlusunun hazırladığı `/opt/blueforce/offline-repo` kopyasını taşıyın; ya da
- **Hedefli LFS çekme (yalnız internet varken):** depo kökünde
```
git lfs pull --include='provisioning/offline-repo/built/pool/*'
```
Bu yalnız teslim havuzunu indirir (`packages/` altındaki kaynak kopyayı çekmez, aynı baytları ikinci kez indirmez).
Paketler gelmediyse kurucu bunu **tek cümleyle** söyler (`Paketler henüz indirilmemiş: … Git LFS işaretçisi`), çünkü havuzdaki dosyalar `.deb` değil 131 baytlık işaretçi metnidir.
2. `sudo bf` — pencere açılır: **Blueforce Field OS — Yönetim**. 2. `sudo bf` — pencere açılır: **Blueforce Field OS — Yönetim**.
3. Üst çubuk **Hedef > Bayi no (8 hane)** alanına numarayı yaz (örn. `12010193`). 3. Üst çubuk **Hedef > Bayi no (8 hane)** alanına numarayı yaz (örn. `12010193`).
4. **Kurulum** sekmesi → **`Bu cihazı kur`** (hiçbir şey yazmayan ön kontrol için: `Kurulum ön kontrolü (--check)`). 4. **Kurulum** sekmesi → **`Bu cihazı kur`** (hiçbir şey yazmayan ön kontrol için: `Kurulum ön kontrolü (--check)`).
@ -177,8 +188,9 @@ Grafik operatör uygulaması Tkinter ile yazılmıştır ve tkinter Ubuntu'da **
## 12. Kontrol Listesi ## 12. Kontrol Listesi
- [ ] Paket seti repoda: `provisioning/offline-repo/packages/` (1321 `.deb`, Git LFS ile takip ediliyor). - [ ] Paket kaynağı repoda: `provisioning/offline-repo/packages/` (1321 `.deb`, Git LFS ile takip ediliyor; teslim kopyası `built/pool/` içindedir).
- [ ] Bundle indeksi repoda: `provisioning/offline-repo/built/` (`Packages`, `Packages.gz`, `packages.lock.tsv`, `SHA256SUMS`, `pool/`). - [ ] Bundle indeksi repoda: `provisioning/offline-repo/built/` (`Packages`, `Packages.gz`, `packages.lock.tsv`, `SHA256SUMS`, `pool/`).
- [ ] Paket yükü sahaya taşındı: yerel ayna (USB ya da `/opt/blueforce/offline-repo`) — klon LFS yükünü **bilerek** getirmez, bu adım atlanırsa kurucu `Paketler henüz indirilmemiş: … Git LFS işaretçisi` ile durur.
- [ ] `repo-manifest.yaml` güncel; pin listesi ile paket havuzu uyuşuyor. - [ ] `repo-manifest.yaml` güncel; pin listesi ile paket havuzu uyuşuyor.
- [ ] Depoda/repoda cihaz kimliği, private key veya kalıcı token yok. - [ ] Depoda/repoda cihaz kimliği, private key veya kalıcı token yok.
- [ ] `python3-tk` ve bağımlılıkları `manifests/base-packages.txt` içinde pinli; `18-final-check` tkinter'ı denetliyor. - [ ] `python3-tk` ve bağımlılıkları `manifests/base-packages.txt` içinde pinli; `18-final-check` tkinter'ı denetliyor.
@ -200,7 +212,7 @@ Grafik operatör uygulaması Tkinter ile yazılmıştır ve tkinter Ubuntu'da **
```mermaid ```mermaid
flowchart LR flowchart LR
M["git checkout (paketler LFS ile)<br/>veya /opt/blueforce/offline-repo"] --> G["sudo bf → Kurulum → 'Bu cihazı kur'"] M["git clone (hafif: LFS smudge kapalı)<br/>+ paket yükü yerel aynadan"] --> G["sudo bf → Kurulum → 'Bu cihazı kur'"]
G --> D{"Kurucu yerel depoyu arar<br/>BF_OFFLINE_REPO → repo/built → /opt/blueforce/offline-repo"} G --> D{"Kurucu yerel depoyu arar<br/>BF_OFFLINE_REPO → repo/built → /opt/blueforce/offline-repo"}
D -->|"hiçbiri yok / eksik"| X["HATA: Yerel paket deposu yok<br/>aranan yollar + ÇIKIŞ NEDENİ · exit 1<br/>sistem değişmez"] D -->|"hiçbiri yok / eksik"| X["HATA: Yerel paket deposu yok<br/>aranan yollar + ÇIKIŞ NEDENİ · exit 1<br/>sistem değişmez"]
D -->|"eksiksiz aday bulundu"| V{"Doğrulama katmanı:<br/>dosya listesi, SHA256SUMS, kilit, pin ↔ pool"} D -->|"eksiksiz aday bulundu"| V{"Doğrulama katmanı:<br/>dosya listesi, SHA256SUMS, kilit, pin ↔ pool"}

View file

@ -178,6 +178,19 @@ validate_offline_repository() {
local repo="$REPO_DIR" manifest pin package version source lpackage lversion larch lsha lsource local repo="$REPO_DIR" manifest pin package version source lpackage lversion larch lsha lsource
[[ "$repo" = /* && -d "$repo" ]] || blocked_bundle 'BF_OFFLINE_REPO mutlak bir yerel dizin olmalıdır.' [[ "$repo" = /* && -d "$repo" ]] || blocked_bundle 'BF_OFFLINE_REPO mutlak bir yerel dizin olmalıdır.'
repository_is_complete "$repo" || blocked_bundle 'Yerel APT indeksi, kilit dosyası ya da checksum kaydı eksik; paket seti tamamlanmamış.' repository_is_complete "$repo" || blocked_bundle 'Yerel APT indeksi, kilit dosyası ya da checksum kaydı eksik; paket seti tamamlanmamış.'
# The pool is stored through Git LFS, so a checkout made with smudge disabled
# (bf-bootstrap does this deliberately: it avoids an 884 MB download) leaves
# 131-byte pointer texts where the .deb files should be. They satisfy `-f`
# above, so without this gate the run would die later inside `sha256sum -c`
# and report a checksum mismatch, which reads as a corrupt bundle instead of
# what it is: payload that was never fetched.
if [[ -e "$repo/pool" ]]; then
while IFS= read -r -d '' _pkgfile; do
if [[ "$(head -c 42 "$_pkgfile" 2>/dev/null)" == 'version https://git-lfs.github.com/spec/v1'* ]]; then
blocked_bundle "Paketler henüz indirilmemiş: depodaki dosyalar .deb değil, Git LFS işaretçisi ($(basename "$_pkgfile")). Çözüm: depo kökünde 'git lfs pull --include=\"provisioning/offline-repo/built/pool/*\"' çalıştırın, sonra bu kurulumu tekrarlayın."
fi
done < <(find "$repo/pool" -maxdepth 1 -type f -name '*.deb' -print0 2>/dev/null)
fi
(cd "$repo" && sha256sum -c SHA256SUMS >/dev/null) || blocked_bundle 'Yerel APT deposunun checksum doğrulaması başarısız.' (cd "$repo" && sha256sum -c SHA256SUMS >/dev/null) || blocked_bundle 'Yerel APT deposunun checksum doğrulaması başarısız.'
awk -F '\t' 'NR == 1 { if ($0 != "package\tversion\tarchitecture\tsha256\tsource") exit 1; next } $1 !~ /^[a-z0-9][a-z0-9+.-]*$/ || $2 == "" || $3 == "" || $4 !~ /^[a-f0-9]{64}$/ || $5 == "" { exit 1 } END { exit NR < 2 }' "$repo/packages.lock.tsv" || blocked_bundle 'Paket kilidi boş ya da biçimi bozuk.' awk -F '\t' 'NR == 1 { if ($0 != "package\tversion\tarchitecture\tsha256\tsource") exit 1; next } $1 !~ /^[a-z0-9][a-z0-9+.-]*$/ || $2 == "" || $3 == "" || $4 !~ /^[a-f0-9]{64}$/ || $5 == "" { exit 1 } END { exit NR < 2 }' "$repo/packages.lock.tsv" || blocked_bundle 'Paket kilidi boş ya da biçimi bozuk.'
# Every locked row must be present in the pool: the lock file is the SBOM the # Every locked row must be present in the pool: the lock file is the SBOM the

View file

@ -156,6 +156,34 @@ if STATE_DIR="$TMP/offline-state" LOG_FILE="$TMP/offline.log" BF_OFFLINE_REPO="$
! grep -Eqi '(curl|wget|https?://|apt[[:space:]]+download)' "$TMP/offline.out" "$TMP/offline.log" 2>/dev/null || fail 'offline rejection must not run or report network package acquisition' ! grep -Eqi '(curl|wget|https?://|apt[[:space:]]+download)' "$TMP/offline.out" "$TMP/offline.log" 2>/dev/null || fail 'offline rejection must not run or report network package acquisition'
pass 'offline installer fails closed before package modules when local repository is absent' pass 'offline installer fails closed before package modules when local repository is absent'
# --- scenario 2b: a Git-LFS pointer is not a package -------------------------
# The bundle pool travels through Git LFS. A checkout made with smudge disabled
# (which bf-bootstrap does deliberately, to avoid an 884 MB download) leaves
# small pointer texts where the .deb files belong. They satisfy every existence
# test, so the installer must recognise them by content and say what to run --
# otherwise the run dies later inside `sha256sum -c` and reads as a corrupt
# bundle rather than unfetched payload.
STUB_REPO="$TMP/stub-offline-repo"
mkdir -p "$STUB_REPO/pool"
printf 'stub index\n' > "$STUB_REPO/Packages"
printf 'stub index\n' > "$STUB_REPO/Packages.gz"
printf 'FAKE-SHA256SUMS\n' > "$STUB_REPO/SHA256SUMS"
printf 'package\tversion\tarchitecture\tsha256\tsource\n' > "$STUB_REPO/packages.lock.tsv"
printf 'curl\t8.5.0-2ubuntu10\tamd64\t%s\tcurl_8.5.0-2ubuntu10_amd64.deb\n' \
'0000000000000000000000000000000000000000000000000000000000000000' >> "$STUB_REPO/packages.lock.tsv"
cat > "$STUB_REPO/pool/curl_8.5.0-2ubuntu10_amd64.deb" <<'POINTER'
version https://git-lfs.github.com/spec/v1
oid sha256:0000000000000000000000000000000000000000000000000000000000000000
size 285342
POINTER
rm -f "$MOCK_APT_CALLS"
if STATE_DIR="$TMP/stub-state" LOG_FILE="$TMP/stub.log" BF_OFFLINE_REPO="$STUB_REPO" "$REPO_ROOT/scripts/install/blueforce-install.sh" --offline --dealer-id "$DEALER" --only 02-system --yes > "$TMP/stub.out" 2>&1; then fail 'offline installer must reject a bundle whose pool holds LFS pointers, not packages'; fi
grep -qi 'indirilmemiş\|işaretçi' "$TMP/stub.out" || fail 'the LFS-pointer refusal must name the cause (pointer text, not a .deb)'
grep -q 'git lfs pull' "$TMP/stub.out" || fail 'the LFS-pointer refusal must tell the operator the command that fixes it'
grep -qi 'checksum' "$TMP/stub.out" && fail 'an LFS pointer must not be reported as a checksum failure; that misdiagnoses unfetched payload as corruption'
[[ ! -e "$MOCK_APT_CALLS" ]] || fail 'an LFS-pointer rejection must not invoke apt-get'
pass 'offline installer recognises a Git-LFS pointer and names the remedy'
# --- scenario 3: activation failure rolls back, phase is preserved ---------- # --- scenario 3: activation failure rolls back, phase is preserved ----------
write_state "$pending_state" write_state "$pending_state"
rm -f "$WG_DIR/wg0.conf" rm -f "$WG_DIR/wg0.conf"