diff --git a/admin/bf-bootstrap.sh b/admin/bf-bootstrap.sh index f124016..d570759 100755 --- a/admin/bf-bootstrap.sh +++ b/admin/bf-bootstrap.sh @@ -77,7 +77,25 @@ EOF say() { printf 'bf-bootstrap: %s\n' "$*"; } warn() { printf 'bf-bootstrap: WARN: %s\n' "$*" >&2; } 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() { if [[ "$CHECK_MODE" -eq 1 ]]; then printf 'bf-bootstrap: check: would run: %s\n' "$*" diff --git a/docs/27-OFFLINE-PROVISIONING.md b/docs/27-OFFLINE-PROVISIONING.md index 112f012..56fd3af 100644 --- a/docs/27-OFFLINE-PROVISIONING.md +++ b/docs/27-OFFLINE-PROVISIONING.md @@ -1,6 +1,6 @@ # 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` > @@ -13,7 +13,7 @@ ## 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 @@ -33,7 +33,9 @@ LİSANS: APT — https://manpages.ubuntu.com/manpages/noble/en/man8/apt.8.html ## 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. @@ -56,7 +58,7 @@ Durum makinesi, "kuruldu" ile "merkezce güvenildi" arasındaki farkı görünü ## 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. - 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) -1. Depoyu getir: `git clone ` (paketler Git LFS ile gelir) — veya repo sorumlusunun bıraktığı kopyada `/opt/blueforce/offline-repo` hazırdır. +1. Depoyu getir: `git clone ` — **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**. 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)`). @@ -177,8 +188,9 @@ Grafik operatör uygulaması Tkinter ile yazılmıştır ve tkinter Ubuntu'da ** ## 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/`). +- [ ] 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. - [ ] 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. @@ -200,7 +212,7 @@ Grafik operatör uygulaması Tkinter ile yazılmıştır ve tkinter Ubuntu'da ** ```mermaid flowchart LR - M["git checkout (paketler LFS ile)
veya /opt/blueforce/offline-repo"] --> G["sudo bf → Kurulum → 'Bu cihazı kur'"] + M["git clone (hafif: LFS smudge kapalı)
+ paket yükü yerel aynadan"] --> G["sudo bf → Kurulum → 'Bu cihazı kur'"] G --> D{"Kurucu yerel depoyu arar
BF_OFFLINE_REPO → repo/built → /opt/blueforce/offline-repo"} D -->|"hiçbiri yok / eksik"| X["HATA: Yerel paket deposu yok
aranan yollar + ÇIKIŞ NEDENİ · exit 1
sistem değişmez"] D -->|"eksiksiz aday bulundu"| V{"Doğrulama katmanı:
dosya listesi, SHA256SUMS, kilit, pin ↔ pool"} diff --git a/scripts/install/blueforce-install.sh b/scripts/install/blueforce-install.sh index ae710dc..0a321a5 100755 --- a/scripts/install/blueforce-install.sh +++ b/scripts/install/blueforce-install.sh @@ -178,6 +178,19 @@ validate_offline_repository() { 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.' 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.' 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 diff --git a/tests/test-field-os-lifecycle.sh b/tests/test-field-os-lifecycle.sh index f6d8df3..ccb9b73 100755 --- a/tests/test-field-os-lifecycle.sh +++ b/tests/test-field-os-lifecycle.sh @@ -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' 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 ---------- write_state "$pending_state" rm -f "$WG_DIR/wg0.conf"