feat(admin): operator console, git-based install, optional credential delivery
Simplifies the primary path from "build a custom ISO" to "install from git and run one menu", which is what the operator actually asked for. The ISO tooling stays in the tree as a secondary/optional route and is not removed. admin/bf-bootstrap.sh -- one command on Ubuntu: - clones the repo to /opt/blueforce-linux or fast-forwards an existing checkout (a dirty tree is reported and skipped, never reset or stashed) - installs git/curl/ansible-core, links admin/bf-* and scripts/*/bf-* into /usr/local/bin, and finishes with the exact next command - --check is read-only, works without root, and mutates nothing; the piped form (curl ... | sudo -S -p '' bash) is guarded against eating the script admin/bf-menu -- numbered operator console (categories 1-5): - 1 installs THIS device locally; 2 and 5 drive the fleet through the existing 21 Ansible playbooks; 3 prepares and verifies central integration without installing a central server; 4 holds local tools - every operation picks one target: a dealer number or an approved wave group. Scope is derived from the playbook file itself, so a playbook whose declared scope disagrees with the console's table is refused rather than run - the command is always shown before it runs; release-gated operations require a typed wave token, production requires a second token, and "all" runs wave-by-wave, stopping at the first failure - audit log at /var/log/blueforce-console.log (mode 600, falls back to $XDG_STATE_HOME when not writable), recording sanitized target/command/exit only: no playbook output, no secrets admin/bf-creds -- optional credential record, DEFAULT OFF: - a bare run generates nothing and reports what already exists (device id, WireGuard public key, private key path only, RustDesk id) - --generate writes /root/blueforce-credentials.txt (0600) and --fetch delivers it to admin/secrets/<dealer>.txt (0700/0600, git-ignored). The secret never reaches stdout, stderr or argv - this does not change the SSH design: password auth stays off and the blueforce account stays locked, per docs/06. It is a local delivery aid only docs/33-OPERATOR-CONSOLE.md -- step-by-step guide for an operator new to Linux and DevOps: which command to type where, what the screen will ask, both end-to-end sample sessions, the five most common errors, and the security rules. Tests: run-all.sh -> 9/9 pass, exit 0 (adds check-admin-static.sh, 159 checks, with negative controls and mutation-proven non-vacuity).
This commit is contained in:
parent
ef102b9657
commit
f8cdd1ed01
12 changed files with 4870 additions and 5 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -41,3 +41,8 @@ id_ed25519*
|
||||||
*credentials*
|
*credentials*
|
||||||
*token*
|
*token*
|
||||||
*.secret
|
*.secret
|
||||||
|
|
||||||
|
# Credential files delivered by `admin/bf-creds --fetch` (<dealer-number>.txt).
|
||||||
|
# They live only in the checkout that runs the delivery, are root-owned on the
|
||||||
|
# device (mode 0600 inside a mode 0700 directory), and are never committed.
|
||||||
|
admin/secrets/
|
||||||
|
|
|
||||||
38
README.md
38
README.md
|
|
@ -10,6 +10,32 @@
|
||||||
|
|
||||||
%100 ücretsiz / self-hosted Linux filo mimarisi: başka bir DevOps mühendisinin sıfırdan kurabileceği doküman setinin ve çalışan iskeletin ana reposu. Dokümanlar Türkçedir; komut, dosya, servis ve terim adları İngilizce kalır.
|
%100 ücretsiz / self-hosted Linux filo mimarisi: başka bir DevOps mühendisinin sıfırdan kurabileceği doküman setinin ve çalışan iskeletin ana reposu. Dokümanlar Türkçedir; komut, dosya, servis ve terim adları İngilizce kalır.
|
||||||
|
|
||||||
|
**Kurulum git tabanlıdır:** depo klonlanır, `admin/bf-bootstrap.sh` operatör araçlarını kurar ve sahada tek giriş noktası olan `sudo bf-menu` kategori menüsü açılır. ISO/firstboot provisioning yolu (bkz. [`docs/26`](docs/26-BLUEFORCE-FIELD-OS-ISO.md), [`docs/27`](docs/27-OFFLINE-PROVISIONING.md)) **ikincil/opsiyoneldir**, silinmez. Sıfırdan başlayan için adım adım rehber: [`docs/33-OPERATOR-CONSOLE.md`](docs/33-OPERATOR-CONSOLE.md).
|
||||||
|
|
||||||
|
## Hızlı Başlangıç (git ile kurulum)
|
||||||
|
|
||||||
|
Kurulumun birincil yolu **git deposudur**; ISO/provisioning yolu (aşağıdaki §Kurulum Akışı, [`docs/26`](docs/26-BLUEFORCE-FIELD-OS-ISO.md)) ikincil ve opsiyoneldir. Sahadaki tek giriş noktası `sudo bf-menu` **operatör konsoludur**: kategoriler numarayla seçilir, komut ezberlenmez.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) Depoyu indir
|
||||||
|
git clone https://github.com/emirhanmamak-blueforce/Blueforce-Linux.git
|
||||||
|
cd Blueforce-Linux
|
||||||
|
|
||||||
|
# 2) Bootstrap — operatör araçlarını ve bf-* komutlarını kurar
|
||||||
|
# Cihaz kimliği (bayi no) İSTEMEZ; disk bölmez, veri silmez, onaysız güncelleme yapmaz.
|
||||||
|
sudo admin/bf-bootstrap.sh
|
||||||
|
# Depoyu klonlamadan tek satır alternatifi:
|
||||||
|
# curl -fsSL https://raw.githubusercontent.com/emirhanmamak-blueforce/Blueforce-Linux/main/admin/bf-bootstrap.sh | sudo bash
|
||||||
|
|
||||||
|
# 3) Operatör konsolunu aç
|
||||||
|
sudo bf-menu
|
||||||
|
```
|
||||||
|
|
||||||
|
Menüden **1) Installation → 2) Install this device** seçilir ve 8 haneli bayi no girilir (ör. `12010193` → cihaz `BF-12010193`, hostname `bf-12010193`). Kurulum sonunda cihaz `PROVISIONED_OFFLINE` olur; `READY` yalnız merkez kanalları doğrulanınca alınır.
|
||||||
|
|
||||||
|
- Sıfırdan başlayan için adım adım rehber (hangi ekranda ne yazılır): [`docs/33-OPERATOR-CONSOLE.md`](docs/33-OPERATOR-CONSOLE.md)
|
||||||
|
- Menü ↔ komut eşlemesi ve kategori haritası aynı dokümanın Ek'lerindedir; özet soru-cevap: [`SUMMARY.md`](SUMMARY.md).
|
||||||
|
|
||||||
## Field OS Nedir?
|
## Field OS Nedir?
|
||||||
|
|
||||||
Blueforce Field OS, AdBlue saha otomasyonundaki ~700 Windows mini PC'yi lisans maliyeti olmayan, merkezi yönetilebilir ve kesintisiz çalışan bir Linux filosuna taşıyan **standarttır**. Üç katmanı vardır:
|
Blueforce Field OS, AdBlue saha otomasyonundaki ~700 Windows mini PC'yi lisans maliyeti olmayan, merkezi yönetilebilir ve kesintisiz çalışan bir Linux filosuna taşıyan **standarttır**. Üç katmanı vardır:
|
||||||
|
|
@ -50,6 +76,8 @@ flowchart TB
|
||||||
|
|
||||||
## Kurulum Akışı (özet)
|
## Kurulum Akışı (özet)
|
||||||
|
|
||||||
|
> **Birincil yol git'tir.** Cihazı kurmanın standart yolu §[Hızlı Başlangıç](#hızlı-başlangıç-git-ile-kurulum) adımlarıdır (depo + `admin/bf-bootstrap.sh` + `sudo bf-menu`, bkz. [`docs/33`](docs/33-OPERATOR-CONSOLE.md)). Aşağıdaki ISO/firstboot akışı **ikincil/opsiyonel** yoldur; silinmemiştir ve offline/air-gapped saha için korunur.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
ISO["Field OS ISO (tek USB)<br/>veya upstream ISO + seed"] --> FB["firstboot<br/>BLUEFORCE FIELD OS<br/>8 haneli bayi no"]
|
ISO["Field OS ISO (tek USB)<br/>veya upstream ISO + seed"] --> FB["firstboot<br/>BLUEFORCE FIELD OS<br/>8 haneli bayi no"]
|
||||||
|
|
@ -97,7 +125,7 @@ Boot kabulü (UEFI/Legacy/Secure Boot) ve air-gapped kurulum henüz doğrulanmad
|
||||||
| Masaüstü kararı | GNOME baseline (K-02); LAB'da GNOME vs XFCE A/B ölçümü yapılır, revizyon yalnız ölçüm verisiyle (K-23) |
|
| Masaüstü kararı | GNOME baseline (K-02); LAB'da GNOME vs XFCE A/B ölçümü yapılır, revizyon yalnız ölçüm verisiyle (K-23) |
|
||||||
| Doküman dili | Türkçe; komut, dosya, servis ve terim adları İngilizce kalır |
|
| Doküman dili | Türkçe; komut, dosya, servis ve terim adları İngilizce kalır |
|
||||||
|
|
||||||
## Doküman Haritası (33 numaralı doküman + şablon)
|
## Doküman Haritası (34 numaralı doküman + şablon)
|
||||||
|
|
||||||
Tüm numaralı dokümanlar `docs/` altındadır ve `docs/_TEMPLATE.md` şablonuna uyar (her dokümanda §1–§13 sırası ve en az bir Mermaid diyagramı vardır).
|
Tüm numaralı dokümanlar `docs/` altındadır ve `docs/_TEMPLATE.md` şablonuna uyar (her dokümanda §1–§13 sırası ve en az bir Mermaid diyagramı vardır).
|
||||||
|
|
||||||
|
|
@ -136,6 +164,7 @@ Tüm numaralı dokümanlar `docs/` altındadır ve `docs/_TEMPLATE.md` şablonun
|
||||||
| 30 | `30-FIELD-OS-RELEASE-MANAGEMENT.md` | Field OS release, manifest ve rollback |
|
| 30 | `30-FIELD-OS-RELEASE-MANAGEMENT.md` | Field OS release, manifest ve rollback |
|
||||||
| 31 | `31-LEARNING-GUIDE.md` | Field OS öğrenme rehberi (LAB pratiği) |
|
| 31 | `31-LEARNING-GUIDE.md` | Field OS öğrenme rehberi (LAB pratiği) |
|
||||||
| 32 | `32-REPOSITORY-AUDIT-AND-CONFLICTS.md` | Depo denetimi, çelişki raporu ve giderim durumu |
|
| 32 | `32-REPOSITORY-AUDIT-AND-CONFLICTS.md` | Depo denetimi, çelişki raporu ve giderim durumu |
|
||||||
|
| 33 | `33-OPERATOR-CONSOLE.md` | Operatör konsolu (`bf-menu`) — sıfırdan başlayan için git kurulumu + menü rehberi |
|
||||||
|
|
||||||
Şablon: `docs/_TEMPLATE.md` (siteye alınmaz). Karar günlüğü: `docs/24-DECISION-LOG.md`. Özet ve soru-cevap: `SUMMARY.md`.
|
Şablon: `docs/_TEMPLATE.md` (siteye alınmaz). Karar günlüğü: `docs/24-DECISION-LOG.md`. Özet ve soru-cevap: `SUMMARY.md`.
|
||||||
|
|
||||||
|
|
@ -144,9 +173,10 @@ Tüm numaralı dokümanlar `docs/` altındadır ve `docs/_TEMPLATE.md` şablonun
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── README.md
|
├── README.md
|
||||||
├── SUMMARY.md # Yönetici özeti, 37 soru-cevap, FREE bileşen listesi
|
├── SUMMARY.md # Yönetici özeti, 39 soru-cevap, FREE bileşen listesi
|
||||||
├── IMPLEMENTATION-REPORT.md # Uygulama raporu (ne yapıldı / hangi testler geçti / LAB bekleyenler)
|
├── IMPLEMENTATION-REPORT.md # Uygulama raporu (ne yapıldı / hangi testler geçti / LAB bekleyenler)
|
||||||
├── docs/ # 33 numaralı doküman (00–32) + _TEMPLATE.md
|
├── docs/ # 34 numaralı doküman (00–33) + _TEMPLATE.md
|
||||||
|
├── admin/ # git'ten tek komut kurulum (bf-bootstrap.sh, bf-creds, bf-menu)
|
||||||
├── provisioning/ # Field OS medya üretimi
|
├── provisioning/ # Field OS medya üretimi
|
||||||
│ ├── autoinstall/ # autoinstall.yaml + user-data + meta-data (interactive storage)
|
│ ├── autoinstall/ # autoinstall.yaml + user-data + meta-data (interactive storage)
|
||||||
│ ├── iso/ # ISO doğrulama + build-blueforce-iso.sh (+ test-iso.sh)
|
│ ├── iso/ # ISO doğrulama + build-blueforce-iso.sh (+ test-iso.sh)
|
||||||
|
|
@ -236,4 +266,4 @@ Maliyet kuralı:
|
||||||
| F5 | Production rollout (~523) | [ ] |
|
| F5 | Production rollout (~523) | [ ] |
|
||||||
| F6 | Gerçek zero-touch PXE (ayrı FAZ) | [ ] |
|
| F6 | Gerçek zero-touch PXE (ayrı FAZ) | [ ] |
|
||||||
|
|
||||||
Repo iskeleti ve uygulama katmanı hazırdır: 33 numaralı doküman, `blueforce-install.sh` + 18 modül, provisioning katmanı (ISO builder, autoinstall, offline-repo sözleşmesi, firstboot, enrollment, release), 21 Ansible playbook + 4 rol, config/monitoring/rustdesk dosyaları ve 8 doğrulama testi bu repodadır. **Field OS kurulum medyası üretilmiştir:** `dist/Blueforce-Field-OS-1.0.0-amd64.iso` (6 481 917 952 byte, SHA256 `cb0cc56f0442fbd1418e0d0f35521522f4ec9aa3f203779a67e96d587d431296`), El Torito BIOS+UEFI korunmuş, `interactive-sections: [storage]` ile operatör disk onayı zorunlu, kimlik/secret gömülü değil. Henüz **hiçbir şey sahaya sürülmedi**: gerçek offline paket deposu yok (pinler `UNPINNED`, `.deb` havuzu boş), merkezi enrollment endpoint'i yok, boot kabulü (UEFI/Legacy/Secure Boot), air-gapped kurulum ve LAB kanıtları alınmadı. Güncel durum, madde madde giderim kaydı ve açık işler [`docs/32`](docs/32-REPOSITORY-AUDIT-AND-CONFLICTS.md) ve [`IMPLEMENTATION-REPORT.md`](IMPLEMENTATION-REPORT.md) içindedir. Sıradaki adımlar [`docs/25`](docs/25-IMPLEMENTATION-ROADMAP.md) yol haritasındadır; sahaya çıkış için önce F2B (offline paket seti + boot kabulü) ve F3 (LAB) kapıları kapanmalıdır.
|
Repo iskeleti ve uygulama katmanı hazırdır: 34 numaralı doküman (00–33), `blueforce-install.sh` + 18 modül, git tabanlı operatör konsolu (`admin/bf-bootstrap.sh`, `admin/bf-menu`, `admin/bf-creds`), provisioning katmanı (ISO builder, autoinstall, offline-repo sözleşmesi, firstboot, enrollment, release), 21 Ansible playbook + 4 rol, config/monitoring/rustdesk dosyaları ve doğrulama testleri bu repodadır. **Field OS kurulum medyası üretilmiştir:** `dist/Blueforce-Field-OS-1.0.0-amd64.iso` (6 481 917 952 byte, SHA256 `cb0cc56f0442fbd1418e0d0f35521522f4ec9aa3f203779a67e96d587d431296`), El Torito BIOS+UEFI korunmuş, `interactive-sections: [storage]` ile operatör disk onayı zorunlu, kimlik/secret gömülü değil. Henüz **hiçbir şey sahaya sürülmedi**: gerçek offline paket deposu yok (pinler `UNPINNED`, `.deb` havuzu boş), merkezi enrollment endpoint'i yok, boot kabulü (UEFI/Legacy/Secure Boot), air-gapped kurulum ve LAB kanıtları alınmadı. Güncel durum, madde madde giderim kaydı ve açık işler [`docs/32`](docs/32-REPOSITORY-AUDIT-AND-CONFLICTS.md) ve [`IMPLEMENTATION-REPORT.md`](IMPLEMENTATION-REPORT.md) içindedir. Sıradaki adımlar [`docs/25`](docs/25-IMPLEMENTATION-ROADMAP.md) yol haritasındadır; sahaya çıkış için önce F2B (offline paket seti + boot kabulü) ve F3 (LAB) kapıları kapanmalıdır.
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@
|
||||||
## 1. Field OS için Kısa Soru-Cevap (2026-09-16 turu)
|
## 1. Field OS için Kısa Soru-Cevap (2026-09-16 turu)
|
||||||
|
|
||||||
- **Blueforce Field OS nedir?** — Ubuntu Server 26.04.1+ LTS üzerinde maintain edilen GNOME katmanı bulunan, çevrimdışı kurulabilen, merkezden yönetilen; onaysız güncelleme yapmayan ve 4 uzak erişim kanalı kullanan **endüstriyel saha işletim sistemi standardıdır**. Üç parçası vardır: zemin (Ubuntu LTS + GNOME + terminale boot), provisioning (tek kurucu + autoinstall/offline APT/firstboot + tek USB Field OS ISO) ve işletme (WireGuard + 4 kanal + Docker/MEG + monitoring + Ansible/Semaphore). **Medya tabanı sapması:** kanonik baseline Ubuntu **Server**'dır; build host'ta checksum'ı doğrulanmış tek medya Ubuntu **Desktop** ISO olduğu için üretilen ilk ISO Desktop tabanlıdır (manifest `baseline_deviation` bloğu). Hedef işletim sistemi yine Server paket setidir (`ubuntu-server` + `ubuntu-desktop-minimal` + GNOME, terminale boot); sapma yalnız medya tabanındadır ve LAB kararı bekler. → [`00`](docs/00-MASTER-PLAN.md), [`01`](docs/01-ARCHITECTURE.md), [`26`](docs/26-BLUEFORCE-FIELD-OS-ISO.md), [`31`](docs/31-LEARNING-GUIDE.md) (K-01, K-02, K-20)
|
- **Blueforce Field OS nedir?** — Ubuntu Server 26.04.1+ LTS üzerinde maintain edilen GNOME katmanı bulunan, çevrimdışı kurulabilen, merkezden yönetilen; onaysız güncelleme yapmayan ve 4 uzak erişim kanalı kullanan **endüstriyel saha işletim sistemi standardıdır**. Üç parçası vardır: zemin (Ubuntu LTS + GNOME + terminale boot), provisioning (tek kurucu + autoinstall/offline APT/firstboot + tek USB Field OS ISO) ve işletme (WireGuard + 4 kanal + Docker/MEG + monitoring + Ansible/Semaphore). **Medya tabanı sapması:** kanonik baseline Ubuntu **Server**'dır; build host'ta checksum'ı doğrulanmış tek medya Ubuntu **Desktop** ISO olduğu için üretilen ilk ISO Desktop tabanlıdır (manifest `baseline_deviation` bloğu). Hedef işletim sistemi yine Server paket setidir (`ubuntu-server` + `ubuntu-desktop-minimal` + GNOME, terminale boot); sapma yalnız medya tabanındadır ve LAB kararı bekler. → [`00`](docs/00-MASTER-PLAN.md), [`01`](docs/01-ARCHITECTURE.md), [`26`](docs/26-BLUEFORCE-FIELD-OS-ISO.md), [`31`](docs/31-LEARNING-GUIDE.md) (K-01, K-02, K-20)
|
||||||
|
- **Nasıl kurulur?** — Kurulumun birincil yolu **git deposudur**: `git clone https://github.com/emirhanmamak-blueforce/Blueforce-Linux.git` → `cd Blueforce-Linux` → `sudo admin/bf-bootstrap.sh` (depoyu klonlamadan tek satır alternatifi: `curl -fsSL https://raw.githubusercontent.com/emirhanmamak-blueforce/Blueforce-Linux/main/admin/bf-bootstrap.sh | sudo bash`) → `sudo bf-menu`. Bootstrap yalnız operatör araçlarını ve `bf-*` komutlarını kurar; **cihaz kimliği (bayi no) İSTEMEZ**, disk bölmez, veri silmez, onaysız güncelleme yapmaz (idempotent). Kimlik ilk `bf-menu` seansında girilir. ISO/firstboot yolu ([`26`](docs/26-BLUEFORCE-FIELD-OS-ISO.md)/[`27`](docs/27-OFFLINE-PROVISIONING.md)) **ikincil/opsiyoneldir** ve silinmez. Ayrıntılı adım adım rehber: [`33`](docs/33-OPERATOR-CONSOLE.md). → [`05`](docs/05-ONE-CLICK-INSTALLER.md), [`33`](docs/33-OPERATOR-CONSOLE.md) (K-09, K-12, K-20)
|
||||||
|
- **Menü nasıl kullanılır?** — Sahadaki tek giriş noktası `sudo bf-menu` **operatör konsoludur**: 5 kategori numarayla seçilir, komut ezberlenmez (**1** Installation, **2** Device operations, **3** Central (preparation), **4** Tools, **5** Device information; ana ekranda `q` = çıkış, alt menülerde `0` = geri). Her komut çalıştırılmadan önce ekranda gösterilir ve onay istenir; yıkıcı işlemler iki kez sorar. Kurulumda "**Dealer number (8 digits)**" sorulur (ör. `12010193` → `BF-12010193` / `bf-12010193`); geçersiz format kurulumu başlatmaz (K-12). Filo işlemleri (kategori 2) hedefi **tek cihaz (bayi no) / grup-dalga (lab, pilot_1, pilot_2, wave_1, wave_2, production) / tüm filo** olarak seçtirir; wave-scoped playbook'ta "tüm filo" dalga dalga ilerler ve her dalga ayrı onaylanır, production için `production` yazmak zorunludur (K-11). Onaysız güncelleme seçeneği **yoktur**; `bf-gui-*` yalnız **yerel fiziksel** ekranı açar/kapatır, RDP (xrdp/xrdp-sesman) her zaman hazırdır (K-22). Şifre üretimi opsiyoneldir ve **varsayılan kapalıdır** (`bf-creds --generate`; parola ekrana/loga gitmez). Denetim log'u `/var/log/blueforce-console.log` (mod `600`). → [`33`](docs/33-OPERATOR-CONSOLE.md), [`09`](docs/09-FLEET-MANAGEMENT.md), [`10`](docs/10-UPDATE-AND-ROLLBACK-POLICY.md) (K-11, K-13, K-22)
|
||||||
- **ISO nasıl build edilir?** — Autoinstall kurulum **motorudur**, Blueforce Field OS ISO ise **paketleme/dağıtım** yöntemidir; ikisi rakip değil, birlikte kullanılır. `provisioning/iso/build-blueforce-iso.sh --upstream-iso <BASE> --checksum-file <SHA256SUMS>` çalıştırılır; builder base ISO'yu doğrular, ağacı çıkarır, autoinstall + firstboot + offline-repo araçlarını ekler, GRUB satırlarına autoinstall yazar ve boot düzenini base ISO'nun kendi `as_mkisofs` reçetesinden yeniden kurar. **ISO üretilmiştir:** `dist/Blueforce-Field-OS-1.0.0-amd64.iso` (6 481 917 952 byte, SHA256 `cb0cc56f…d431296`), El Torito BIOS+UEFI korunmuş, `/autoinstall.yaml` medya kökünde. Boot kabulü (UEFI/Legacy/Secure Boot) ve air-gapped kurulum hâlâ LAB işidir; resmi Ubuntu ISO + NoCloud seed USB yedek/geri dönüş yoludur. → [`26`](docs/26-BLUEFORCE-FIELD-OS-ISO.md), [`32`](docs/32-REPOSITORY-AUDIT-AND-CONFLICTS.md) (K-15, K-20)
|
- **ISO nasıl build edilir?** — Autoinstall kurulum **motorudur**, Blueforce Field OS ISO ise **paketleme/dağıtım** yöntemidir; ikisi rakip değil, birlikte kullanılır. `provisioning/iso/build-blueforce-iso.sh --upstream-iso <BASE> --checksum-file <SHA256SUMS>` çalıştırılır; builder base ISO'yu doğrular, ağacı çıkarır, autoinstall + firstboot + offline-repo araçlarını ekler, GRUB satırlarına autoinstall yazar ve boot düzenini base ISO'nun kendi `as_mkisofs` reçetesinden yeniden kurar. **ISO üretilmiştir:** `dist/Blueforce-Field-OS-1.0.0-amd64.iso` (6 481 917 952 byte, SHA256 `cb0cc56f…d431296`), El Torito BIOS+UEFI korunmuş, `/autoinstall.yaml` medya kökünde. Boot kabulü (UEFI/Legacy/Secure Boot) ve air-gapped kurulum hâlâ LAB işidir; resmi Ubuntu ISO + NoCloud seed USB yedek/geri dönüş yoludur. → [`26`](docs/26-BLUEFORCE-FIELD-OS-ISO.md), [`32`](docs/32-REPOSITORY-AUDIT-AND-CONFLICTS.md) (K-15, K-20)
|
||||||
- **Offline kurulum nasıl çalışır?** — Sürümlü medya + offline APT snapshot kullanılır; `blueforce-install.sh --offline` yalnız yerel `file:` APT kaynağından paket kurar (uzak indirme, key download, `apt download` yok) ve kurulum `PROVISIONED_OFFLINE` durumuna gelir. Subiquity'nin `apt.fallback` varsayılanı zaten `offline-install`'dır. Medya manifestlerinde somut `package=version` pinleri yoksa kurucu `OFFLINE BLOCKED` ile durur — "offline olur" demek için paket seti gerçekten dolu olmalıdır. → [`27`](docs/27-OFFLINE-PROVISIONING.md), [`31`](docs/31-LEARNING-GUIDE.md) (K-16, K-20, K-21)
|
- **Offline kurulum nasıl çalışır?** — Sürümlü medya + offline APT snapshot kullanılır; `blueforce-install.sh --offline` yalnız yerel `file:` APT kaynağından paket kurar (uzak indirme, key download, `apt download` yok) ve kurulum `PROVISIONED_OFFLINE` durumuna gelir. Subiquity'nin `apt.fallback` varsayılanı zaten `offline-install`'dır. Medya manifestlerinde somut `package=version` pinleri yoksa kurucu `OFFLINE BLOCKED` ile durur — "offline olur" demek için paket seti gerçekten dolu olmalıdır. → [`27`](docs/27-OFFLINE-PROVISIONING.md), [`31`](docs/31-LEARNING-GUIDE.md) (K-16, K-20, K-21)
|
||||||
- **Device state'leri nedir?** — Üç durum, tek sıra: `PROVISIONED_OFFLINE` → `ENROLLED` → `READY`. Tek kaynak `/var/lib/blueforce/state.json`, okuma komutu `bf-enrollment-status`. `READY` **tek durum değildir**: "kuruldu" READY değildir, "token alındı" READY değildir; yalnız WireGuard + merkezi remote + monitoring kanıtı doğrulanınca READY yazılır ve `bf-check-ready` bu kanıtı her seferinde yeniden denetler. → [`27`](docs/27-OFFLINE-PROVISIONING.md), [`29`](docs/29-DEVICE-ENROLLMENT.md) (K-16, K-17, K-21)
|
- **Device state'leri nedir?** — Üç durum, tek sıra: `PROVISIONED_OFFLINE` → `ENROLLED` → `READY`. Tek kaynak `/var/lib/blueforce/state.json`, okuma komutu `bf-enrollment-status`. `READY` **tek durum değildir**: "kuruldu" READY değildir, "token alındı" READY değildir; yalnız WireGuard + merkezi remote + monitoring kanıtı doğrulanınca READY yazılır ve `bf-check-ready` bu kanıtı her seferinde yeniden denetler. → [`27`](docs/27-OFFLINE-PROVISIONING.md), [`29`](docs/29-DEVICE-ENROLLMENT.md) (K-16, K-17, K-21)
|
||||||
|
|
@ -22,7 +24,7 @@
|
||||||
- **Field OS version nasıl takip edilir?** — Cihazda `bf-release` komutu okunur; kaynak sırası `BF_RELEASE_MANIFEST` → `/etc/blueforce/release-manifest.yaml` → legacy `/etc/blueforce-release` → repo manifesti. Manifest `status: skeleton` ise komut `UNAPPROVED_SKELETON` yazar ve **exit≠0** verir: iskelet medya dağıtılabilir release sayılmaz. Gerçek sürüm kimliği immutable release manifestiyle (ISO/seed/offline APT snapshot/installer/Ansible commit + checksum) eşleşir. → [`30`](docs/30-FIELD-OS-RELEASE-MANAGEMENT.md) (K-18, K-24)
|
- **Field OS version nasıl takip edilir?** — Cihazda `bf-release` komutu okunur; kaynak sırası `BF_RELEASE_MANIFEST` → `/etc/blueforce/release-manifest.yaml` → legacy `/etc/blueforce-release` → repo manifesti. Manifest `status: skeleton` ise komut `UNAPPROVED_SKELETON` yazar ve **exit≠0** verir: iskelet medya dağıtılabilir release sayılmaz. Gerçek sürüm kimliği immutable release manifestiyle (ISO/seed/offline APT snapshot/installer/Ansible commit + checksum) eşleşir. → [`30`](docs/30-FIELD-OS-RELEASE-MANAGEMENT.md) (K-18, K-24)
|
||||||
- **Operatör hangi rehberden öğrenir?** — Linux, ağ, otomasyon, ISO, enrollment ve rollback pratikleri yalnız LAB'da [`31`](docs/31-LEARNING-GUIDE.md) ile çalışılır; komutlar örnektir ve LAB doğrulaması gerektirir.
|
- **Operatör hangi rehberden öğrenir?** — Linux, ağ, otomasyon, ISO, enrollment ve rollback pratikleri yalnız LAB'da [`31`](docs/31-LEARNING-GUIDE.md) ile çalışılır; komutlar örnektir ve LAB doğrulaması gerektirir.
|
||||||
|
|
||||||
Bu 10 soru + öğrenme rehberi sorusu, karar günlüğünde S-28…S-37 olarak izlenir ([Ek B-2](docs/24-DECISION-LOG.md)).
|
Bu 12 soru + öğrenme rehberi sorusu, karar günlüğünde S-28…S-37 olarak izlenir ([Ek B-2](docs/24-DECISION-LOG.md)); bu tura `docs/33` ile **"Nasıl kurulur?"** ve **"Menü nasıl kullanılır?"** soruları eklendi (yukarıdaki ilk bloğun devamı).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -169,6 +171,7 @@ kaynak URL'ler [`24 Ek A`](docs/24-DECISION-LOG.md).
|
||||||
## Ek: QA Doğrulama (2026-09-16 güncellemesi)
|
## Ek: QA Doğrulama (2026-09-16 güncellemesi)
|
||||||
|
|
||||||
- **33 numaralı doküman mevcut:** `docs/00` … `docs/32` (+ `docs/_TEMPLATE.md`); kanonik set `00`–`31`, `32` denetim/çelişki raporudur.
|
- **33 numaralı doküman mevcut:** `docs/00` … `docs/32` (+ `docs/_TEMPLATE.md`); kanonik set `00`–`31`, `32` denetim/çelişki raporudur.
|
||||||
|
- **Ek (operatör konsolu):** `docs/33-OPERATOR-CONSOLE.md` eklendi (§1–§13 + Mermaid; git kurulumu + `bf-menu` menü rehberi). Numaralı set artık `docs/00` … `docs/33` (34 dosya).
|
||||||
- **13 başlık:** 33 dosyanın tamamında §1–§13 sıralı ve eksiksiz (bu turda `docs/32` §1–§13 yapısına getirildi).
|
- **13 başlık:** 33 dosyanın tamamında §1–§13 sıralı ve eksiksiz (bu turda `docs/32` §1–§13 yapısına getirildi).
|
||||||
- **Mermaid:** 33/33 dosyada ≥1 diyagram; toplam 35 blok (30 dosyada 1, birkaçında 2+).
|
- **Mermaid:** 33/33 dosyada ≥1 diyagram; toplam 35 blok (30 dosyada 1, birkaçında 2+).
|
||||||
- **BF-/bf- tutarlılığı:** insan arayüzü `BF-<no>`, makine adı `bf-<no>`; aykırı kullanım yok (kalan `BF-xxxx`/`BF-no` ifadeleri şablon yer tutucudur).
|
- **BF-/bf- tutarlılığı:** insan arayüzü `BF-<no>`, makine adı `bf-<no>`; aykırı kullanım yok (kalan `BF-xxxx`/`BF-no` ifadeleri şablon yer tutucudur).
|
||||||
|
|
|
||||||
315
admin/README.md
Normal file
315
admin/README.md
Normal file
|
|
@ -0,0 +1,315 @@
|
||||||
|
# Blueforce operator console (`bf-menu`)
|
||||||
|
|
||||||
|
Numbered terminal console for the Blueforce field fleet. One entry point for the
|
||||||
|
operator: no command has to be memorised, every command is printed before it
|
||||||
|
runs, and the dangerous ones ask twice.
|
||||||
|
|
||||||
|
Three layers, one screen:
|
||||||
|
|
||||||
|
| Layer | What it is | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| Local install | This device, brought to the Field OS standard | category **1** |
|
||||||
|
| Fleet operations | The repository's existing Ansible playbooks, always with an explicit `--limit` | category **2** |
|
||||||
|
| Read-only information | Device and fleet state, no change anywhere | categories **3, 4, 5** |
|
||||||
|
|
||||||
|
The console is a thin front end. It does not reimplement installer logic, does
|
||||||
|
not invent playbooks, and does not talk to devices itself: it selects the right
|
||||||
|
`bf-*` tool, playbook and target, then executes exactly that.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Requirements
|
||||||
|
|
||||||
|
- Ubuntu LTS field device or admin workstation (the console is plain bash 4+).
|
||||||
|
- `git` for the checkout; `ansible-core`/`ansible` **only** on the machine that
|
||||||
|
drives fleet operations (categories 2 and 3). A field device that only installs
|
||||||
|
itself needs neither Ansible nor an inventory.
|
||||||
|
- `sudo` for category 1 (local install) and for writing to
|
||||||
|
`/var/log/blueforce-console.log`.
|
||||||
|
- Optional, per category: `python3` (reads `state.json`), `wireguard-tools`
|
||||||
|
(`wg`), `systemd`, `dpkg`.
|
||||||
|
|
||||||
|
## 2. Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url> Blueforce-Linux
|
||||||
|
cd Blueforce-Linux
|
||||||
|
|
||||||
|
sudo admin/bf-bootstrap.sh # optional: links the tools into /usr/local/bin
|
||||||
|
sudo admin/bf-menu # or: sudo bf-menu, once bootstrap ran
|
||||||
|
```
|
||||||
|
|
||||||
|
An install is `git`-based on purpose: the running revision is traceable with
|
||||||
|
`git log`, and the device identity (dealer number) is never baked into an image
|
||||||
|
— it is typed in the first console session. ISO/firstboot remain a secondary,
|
||||||
|
optional path (`docs/26`, `docs/27`).
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help usage
|
||||||
|
-V, --version console version
|
||||||
|
--repo PATH repository root (default: parent of this script, symlinks resolved)
|
||||||
|
--inventory PATH Ansible inventory (default: <repo>/ansible/inventory/hosts.yml)
|
||||||
|
--log-file PATH audit log (default: /var/log/blueforce-console.log)
|
||||||
|
--dry-run print every command instead of running it
|
||||||
|
--no-color disable ANSI colors (NO_COLOR is honored too)
|
||||||
|
--list print the operation catalogue and exit (non-interactive)
|
||||||
|
--list-targets print inventory groups and host counts, then exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Environment: `BF_REPO`, `BF_INVENTORY`, `BF_CONSOLE_LOG` mirror the options.
|
||||||
|
|
||||||
|
Exit status: `0` success or clean quit, `1` failure, `2` usage error.
|
||||||
|
|
||||||
|
## 3. Menu map
|
||||||
|
|
||||||
|
`0` is **Back** on every screen, `q` is **Quit** on every screen. The console
|
||||||
|
never changes anything from a menu that only displays information.
|
||||||
|
|
||||||
|
**1) Installation — this device, locally**
|
||||||
|
|
||||||
|
| # | Action | Under the hood |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Pre-flight check | `blueforce-install.sh --dealer-id <no> --check` (writes nothing) |
|
||||||
|
| 2 | Install this device | `--dealer-id <no> [--offline] [--resume]` |
|
||||||
|
| 3 | Resume a failed run | `--dealer-id <no> --resume` |
|
||||||
|
| 4 | Run a single module | `--dealer-id <no> --only <NN-name>` |
|
||||||
|
| 5 | Show module state | `/var/lib/blueforce/install-state` |
|
||||||
|
| 6 | Show the installer log | `tail -n 40 /var/log/blueforce-install.log` |
|
||||||
|
|
||||||
|
Dealer numbers are validated against `^[0-9]{8}$` before anything runs; a wrong
|
||||||
|
format stops the flow immediately. Actions 2–4 require root and will say so.
|
||||||
|
|
||||||
|
**2) Device operations — the fleet, via Ansible**
|
||||||
|
|
||||||
|
*2.1 Read-only reports* (11 operations, nothing is changed):
|
||||||
|
|
||||||
|
| # | Operation | Playbook |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Ping / reachability | `bf-ping.yml` |
|
||||||
|
| 2 | Uptime | `bf-uptime.yml` |
|
||||||
|
| 3 | Disk usage | `bf-disk-check.yml` |
|
||||||
|
| 4 | Docker status | `bf-docker-status.yml` |
|
||||||
|
| 5 | Package versions | `bf-package-check.yml` |
|
||||||
|
| 6 | Pending security updates | `bf-security-updates-check.yml` |
|
||||||
|
| 7 | Provisioning phase report | `bf-provisioning-status.yml` |
|
||||||
|
| 8 | Enrollment + central evidence | `bf-enrollment-status.yml` |
|
||||||
|
| 9 | Field OS version report | `bf-fieldos-version.yml` |
|
||||||
|
| 10 | Remote access channels | `bf-remote-channels-check.yml` |
|
||||||
|
| 11 | Offline readiness report | `bf-offline-ready-check.yml` |
|
||||||
|
|
||||||
|
*2.2 Maintenance* (10 operations, destructive ones ask twice):
|
||||||
|
|
||||||
|
| # | Operation | Playbook | Scope |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | Restart RDP | `bf-rdp-restart.yml` | simple |
|
||||||
|
| 2 | Restart RustDesk | `bf-rustdesk-restart.yml` | simple |
|
||||||
|
| 3 | Restart WireGuard | `bf-wireguard-restart.yml` | simple |
|
||||||
|
| 4 | Restart an allowlisted service | `bf-service-restart.yml` | simple |
|
||||||
|
| 5 | GUI off (headless) | `bf-gui-off.yml` | wave |
|
||||||
|
| 6 | GUI on (maintenance) | `bf-gui-on.yml` | wave |
|
||||||
|
| 7 | Collect logs / support bundle | `bf-collect-logs.yml` | wave |
|
||||||
|
| 8 | Run an approved script | `bf-run-script.yml` | wave |
|
||||||
|
| 9 | Controlled reboot | `bf-reboot.yml` | release |
|
||||||
|
| 10 | Deploy approved update | `bf-deploy-update.yml` | release |
|
||||||
|
|
||||||
|
*2.3 Show inventory targets* lists the waves with their device counts.
|
||||||
|
|
||||||
|
The console cross-checks its own table against the playbook file before running
|
||||||
|
(see §5); a playbook whose gates changed refuses to run rather than run ungated.
|
||||||
|
|
||||||
|
**3) Central (preparation)** — verifies and prepares central integration; it
|
||||||
|
installs no central component. Central readiness report (inventory, placeholders
|
||||||
|
in `group_vars/all.yml`, admin key path), configured endpoints, a read-only
|
||||||
|
DNS/TCP probe of those endpoints, the per-wave gate state, and the preparation
|
||||||
|
checklist.
|
||||||
|
|
||||||
|
**4) Tools** — `git pull --ff-only` of the checkout, environment/reference info,
|
||||||
|
the operation catalogue, the local gates (`bf-check-local`, `bf-check-enrollment`,
|
||||||
|
`bf-check-ready`), `bf-diagnostics`, `bf-live-hw-check`, `bf-remote-status --check`,
|
||||||
|
`bf-release`, `bf-hardware-inventory.sh`, `bf-support-bundle`, and the console's
|
||||||
|
own log.
|
||||||
|
|
||||||
|
**5) Device information** — identity (hostname, `BF-<no>`, dealer number),
|
||||||
|
`bf-status`, provisioning/enrollment state, network (addresses, routes, DNS,
|
||||||
|
WireGuard **peer counts and handshake ages**), disk/memory/CPU, installed pinned
|
||||||
|
packages, service status, recent journal errors, plus the fleet inventory summary
|
||||||
|
and a "resolve a target" helper that shows the exact `--limit` for copy/paste.
|
||||||
|
|
||||||
|
## 4. Target selection
|
||||||
|
|
||||||
|
Every device operation asks for a target first — never a default:
|
||||||
|
|
||||||
|
```
|
||||||
|
Target selection (mode: any|wave|release)
|
||||||
|
1) Single device by dealer number -> bf-<8 digits>
|
||||||
|
2) Group / wave -> lab, pilot_1, pilot_2, wave_1, wave_2, production
|
||||||
|
3) All devices in the fleet -> --limit all
|
||||||
|
9) Type the target directly -> dealer number, group, hostname or 'all'
|
||||||
|
0) Back q) Quit
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules the console enforces:
|
||||||
|
|
||||||
|
- **Single device** — the dealer number must match `^[0-9]{8}$` and the device
|
||||||
|
must exist in the inventory. The wave is derived from the inventory, so a
|
||||||
|
wave-scoped playbook gets `-e operation_wave=<its wave>`. A device outside
|
||||||
|
every wave cannot be used for a wave-scoped operation.
|
||||||
|
- **Group / wave** — the group must exist in the inventory. For wave-scoped and
|
||||||
|
release-gated operations only the six approved waves are offered.
|
||||||
|
- **All devices** — allowed for simple and wave-scoped playbooks. It shows how
|
||||||
|
many devices are affected and requires typing `all` before anything runs. For
|
||||||
|
wave-scoped playbooks the fleet is processed **one wave at a time** in
|
||||||
|
`lab → pilot_1 → pilot_2 → wave_1 → wave_2 → production`, each wave shown and
|
||||||
|
confirmed on its own, stopping at the first failure so no later wave is touched.
|
||||||
|
- **Release-gated playbooks** (`bf-reboot.yml`, `bf-deploy-update.yml`) accept an
|
||||||
|
approved wave group only: no single device, no fleet-wide run. That is the
|
||||||
|
playbooks' own gate (docs/10) and the console does not work around it.
|
||||||
|
|
||||||
|
## 5. What gets added to a command
|
||||||
|
|
||||||
|
The console derives the gate variables from the playbook file itself (its
|
||||||
|
non-comment lines), so the table in the menu can never silently drop a gate:
|
||||||
|
|
||||||
|
| Scope | Detected by | Variables added |
|
||||||
|
|---|---|---|
|
||||||
|
| `simple` | neither wave variable | only `--limit` |
|
||||||
|
| `wave` | uses `operation_wave` | `-e operation_wave=<wave> -e operation_confirmed=true`, plus `-e production_override=true` for the production wave |
|
||||||
|
| `release` | uses `release_wave` | `-e release_wave=<wave>`, `-e reboot_confirmed=true` / `-e wave_gate_confirmed=true` when the playbook declares them, plus the production override for the production wave |
|
||||||
|
|
||||||
|
Confirmations, in order:
|
||||||
|
|
||||||
|
1. the target and the full command are printed;
|
||||||
|
2. for `release` scope, the **wave gate** must be typed as `GATE` (the console
|
||||||
|
never sets `wave_gate_confirmed=true` on its own — only after the previous
|
||||||
|
wave's health gate and central approval, docs/10);
|
||||||
|
3. for the production wave, the word `production` must be typed;
|
||||||
|
4. for fleet-wide scope, the word `all` must be typed;
|
||||||
|
5. `y` runs, `c` runs with `--check` (dry run of the playbook), anything else cancels.
|
||||||
|
|
||||||
|
Extra variables are validated with the same rules the playbooks enforce, so the
|
||||||
|
operator gets a clear message instead of an Ansible assertion failure: package
|
||||||
|
pins must be `package=version`; service names must be in the playbook allowlist;
|
||||||
|
script paths must be inside `/opt/blueforce/bin` or `/usr/local/sbin`; the reboot
|
||||||
|
grace period must be a whole number of seconds.
|
||||||
|
|
||||||
|
## 6. Dry runs
|
||||||
|
|
||||||
|
- `--dry-run` prints every command and executes nothing.
|
||||||
|
- Answering `c` at the run prompt executes the command with `--check` appended,
|
||||||
|
which is a real Ansible dry run.
|
||||||
|
- Category 1's pre-flight already is the installer's own `--check`.
|
||||||
|
|
||||||
|
## 7. Audit log
|
||||||
|
|
||||||
|
Default `/var/log/blueforce-console.log`, created with mode **600** (it is
|
||||||
|
re-`chmod`ed to 600 if it already exists). If it is not writable, the console
|
||||||
|
warns and falls back to `$XDG_STATE_HOME/blueforce/blueforce-console.log`; it
|
||||||
|
never logs into a world-readable file.
|
||||||
|
|
||||||
|
Each run records one line per event: session start, target, the exact command,
|
||||||
|
and the exit status. It does **not** store raw playbook output: tool output may
|
||||||
|
contain whatever a device script prints, and copying it into a log is how a
|
||||||
|
secret ends up on disk. Output is shown on screen and stays there.
|
||||||
|
|
||||||
|
Before anything reaches the log it passes a single sanitizer that redacts
|
||||||
|
`password=`, `token=`, `secret=`, `api_key=`, `private_key=`, `passphrase=`,
|
||||||
|
`psk=` and `BEGIN … PRIVATE KEY` payloads. The console itself never asks for a
|
||||||
|
secret: no password, token or private key is typed into or stored by it.
|
||||||
|
|
||||||
|
## 8. Security notes
|
||||||
|
|
||||||
|
- **No secret material is read, printed or logged.** The console never runs bare
|
||||||
|
`wg show` (private and preshared keys stay off the screen) — only the listen
|
||||||
|
port, peer count and handshake ages.
|
||||||
|
- **`state.json` is read through a whitelist** of non-secret keys (`phase`,
|
||||||
|
`enrollment_status`, `fleet_status`, `device_id`, `provisioning_id`). A future
|
||||||
|
token field cannot leak into the UI or the log.
|
||||||
|
- **No ungated update.** There is no `apt upgrade`, no `latest` pull and no
|
||||||
|
free-form command anywhere in the menu; updates go through the release-gated
|
||||||
|
playbook or not at all (docs/10).
|
||||||
|
- **Destructive actions ask twice**: a warning plus an explicit `y`, and the
|
||||||
|
highest-risk scopes need a typed word (`GATE`, `production`, `all`, or the wave
|
||||||
|
name in a fleet-wide loop).
|
||||||
|
- **Fail-closed on drift**: if the playbook's declared gates do not match the
|
||||||
|
console's expectations, the console refuses to run that playbook at all.
|
||||||
|
- **Root is used where the underlying tool needs it** (local installer, support
|
||||||
|
bundle). Read-only information screens are careful about what they read.
|
||||||
|
- The console **never edits the inventory**. Adding a device stays a reviewed
|
||||||
|
edit of `ansible/inventory/hosts.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
pilot_1:
|
||||||
|
hosts:
|
||||||
|
bf-12010193:
|
||||||
|
ansible_host: 10.42.1.20
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Deliberately out of scope
|
||||||
|
|
||||||
|
- **No distro/ISO build.** ISO and firstboot remain a secondary path (`docs/26`,
|
||||||
|
`docs/27`); the primary install path is `git clone` + install. Category 4 does
|
||||||
|
not offer `provisioning/iso/build-blueforce-iso.sh`.
|
||||||
|
- **No password generation.** Credential files are the separate, optional
|
||||||
|
`admin/bf-creds` tool and default to **off**; the console does not call it.
|
||||||
|
- **No central server installation.** Category 3 verifies and prepares;
|
||||||
|
installing the hub, monitoring or the token service is infrastructure work.
|
||||||
|
- **No new playbooks, no free-form commands.** If an operation is not one of the
|
||||||
|
21 existing playbooks, the console does not fake it.
|
||||||
|
|
||||||
|
## 10. Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Why | What to do |
|
||||||
|
|---|---|---|
|
||||||
|
| `ansible-playbook was not found on this machine` | categories 2/3 need Ansible on the driving machine | `sudo apt install ansible` (or `pipx install ansible-core`), or run the command from another admin host |
|
||||||
|
| `No Ansible inventory found under …` | no `ansible/inventory/hosts.yml` yet | `cp ansible/inventory/hosts.example.yml ansible/inventory/hosts.yml`, then fill in real `bf-<no>` hosts |
|
||||||
|
| Only `hosts.example.yml` exists | it holds example IDs and is never used automatically | copy it and replace the example devices |
|
||||||
|
| `Invalid dealer number '<x>'` | must be exactly 8 digits | type the number as printed on the device record, leading zeros included |
|
||||||
|
| `bf-<no> is not in the inventory` | the device is not registered | add it to the right wave group (§8), then retry |
|
||||||
|
| `… is not an approved wave` | a wave-scoped or release-gated operation got a non-wave group | pick one of `lab, pilot_1, pilot_2, wave_1, wave_2, production` |
|
||||||
|
| `Refusing to run <playbook>: the console expects scope …` | the playbook's gates changed since the console table was written | update `admin/bf-menu` before using that playbook |
|
||||||
|
| `This operation needs root on THIS device` | local install runs as root | `sudo bf-menu` |
|
||||||
|
| Log warning about `/var/log/blueforce-console.log` | not writable, another admin owns it, or the mode is wrong | run with `sudo`, or pass `--log-file <path>` |
|
||||||
|
|
||||||
|
## 11. Files
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|---|---|
|
||||||
|
| `admin/bf-menu` | entry point: menu screens, operation catalogue, command building, confirmations, audit log |
|
||||||
|
| `admin/lib/tui.sh` | presentation and input helpers (screens, items, prompts, confirmations, messages) |
|
||||||
|
| `admin/lib/targets.sh` | device identity, inventory parsing, target selection, `--limit` construction |
|
||||||
|
|
||||||
|
Library contract: `bf-menu` sets `BF_REPO`/`BF_INVENTORY` and sources both
|
||||||
|
libraries; `targets.sh` fills `BF_TARGET_KIND`, `BF_TARGET_LIMIT`,
|
||||||
|
`BF_TARGET_WAVE`, `BF_TARGET_COUNT`, `BF_TARGET_LABEL` and never renders UI or
|
||||||
|
executes anything; `tui.sh` never exits the process except in `tui_die`.
|
||||||
|
|
||||||
|
Both libraries are safe to source on their own, which is how the target logic and
|
||||||
|
the inventory parser can be tested without a terminal.
|
||||||
|
|
||||||
|
## 12. Verifying a change
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n admin/bf-menu admin/lib/tui.sh admin/lib/targets.sh
|
||||||
|
shellcheck -x admin/bf-menu admin/lib/tui.sh admin/lib/targets.sh
|
||||||
|
admin/bf-menu --list # catalogue, no inventory needed
|
||||||
|
admin/bf-menu --list-targets # groups, hosts and counts
|
||||||
|
```
|
||||||
|
|
||||||
|
`--list` and `--list-targets` are the non-interactive entry points: they exercise
|
||||||
|
the catalogue and the inventory parser without touching a device.
|
||||||
|
|
||||||
|
The console was checked on an admin workstation with a stub `ansible-playbook`
|
||||||
|
that echoes its arguments, covering: every scope/target combination and the exact
|
||||||
|
`-e` variables produced, the refusal paths (single device for release scope,
|
||||||
|
fleet-wide release run, wrong wave for a wave-scoped run, bad dealer number,
|
||||||
|
unknown device, bad package pin, service outside the allowlist, script outside
|
||||||
|
the approved directories, production/gate/fleet-wide words withheld), `--dry-run`
|
||||||
|
executing nothing, `--check` being appended, the missing-Ansible and
|
||||||
|
missing-inventory explanations, the log's 600 mode and its redaction of a test
|
||||||
|
secret, and clean EOF behaviour on a closed stdin. Playbook coverage was
|
||||||
|
cross-checked programmatically: all 21 playbooks in `ansible/playbooks/` appear
|
||||||
|
exactly once and every declared scope matches the playbook's own gate variables.
|
||||||
|
Real devices have not been touched yet — the LAB run is what turns these
|
||||||
|
expectations into evidence.
|
||||||
346
admin/bf-bootstrap.sh
Executable file
346
admin/bf-bootstrap.sh
Executable file
|
|
@ -0,0 +1,346 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# bf-bootstrap.sh — one-command install of the Blueforce Linux admin toolbox.
|
||||||
|
#
|
||||||
|
# Documented entry points:
|
||||||
|
# curl -fsSL https://raw.githubusercontent.com/emirhanmamak-blueforce/Blueforce-Linux/main/admin/bf-bootstrap.sh | sudo bash
|
||||||
|
# sudo admin/bf-bootstrap.sh # from a checkout
|
||||||
|
#
|
||||||
|
# Steps, in order:
|
||||||
|
# 1. root check (apply mode only; --check is read-only and needs no root),
|
||||||
|
# 2. Ubuntu release check against the supported baseline (24.04 / 26.04),
|
||||||
|
# 3. dependency install: git, curl, ansible-core,
|
||||||
|
# 4. clone the repository into <dir> (default /opt/blueforce-linux) or
|
||||||
|
# fast-forward an existing checkout (never resets, never discards work),
|
||||||
|
# 5. link admin/bf-*, scripts/diagnostics/bf-* and scripts/maintenance/bf-*
|
||||||
|
# into /usr/local/bin (idempotent; a re-run refreshes the links),
|
||||||
|
# 6. make admin/bf-menu executable so `sudo bf-menu` starts the on-device
|
||||||
|
# installer menu,
|
||||||
|
# 7. print a clear summary.
|
||||||
|
#
|
||||||
|
# Safety
|
||||||
|
# * This script writes no secret and installs code only: credentials stay on
|
||||||
|
# the device and are never fetched by the installer (see admin/bf-creds).
|
||||||
|
# * Re-running it is safe: the checkout is fast-forwarded and the tool links
|
||||||
|
# are re-created. Nothing is deleted and no `git reset` is ever performed,
|
||||||
|
# so local work in the checkout is never discarded.
|
||||||
|
# * --check reports the same plan without touching the system.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_URL="${BF_REPO_URL:-https://github.com/emirhanmamak-blueforce/Blueforce-Linux.git}"
|
||||||
|
BRANCH_DEFAULT="main"
|
||||||
|
INSTALL_DIR_DEFAULT="/opt/blueforce-linux"
|
||||||
|
BIN_DIR="${BF_BIN_DIR:-/usr/local/bin}"
|
||||||
|
SUPPORTED_UBUNTU=("24.04" "26.04")
|
||||||
|
# Command name / apt package name pairs for the installer prerequisites.
|
||||||
|
DEPENDENCY_COMMANDS=(git curl ansible-playbook)
|
||||||
|
DEPENDENCY_PACKAGES=(git curl ansible-core)
|
||||||
|
|
||||||
|
CHECK_MODE=0
|
||||||
|
ASSUME_YES=0
|
||||||
|
BRANCH="$BRANCH_DEFAULT"
|
||||||
|
DIR=""
|
||||||
|
LINKED=0
|
||||||
|
MENU_READY=0
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: sudo bf-bootstrap.sh [--check] [--dir DIR] [--branch BRANCH] [--yes] [--help]
|
||||||
|
|
||||||
|
Install (or refresh) the Blueforce Linux admin toolbox from git: fetch the
|
||||||
|
repository, install the prerequisites, link the bf-* tools into
|
||||||
|
/usr/local/bin, and make `sudo bf-menu` available for the on-device setup.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--check Read-only: report what would happen. It never mutates
|
||||||
|
anything and never requires root.
|
||||||
|
--dir DIR Checkout directory. Default: the checkout this script lives
|
||||||
|
in, otherwise /opt/blueforce-linux.
|
||||||
|
--branch BRANCH Branch to track (default: main).
|
||||||
|
--yes, -y Do not ask for confirmation.
|
||||||
|
--help, -h Show this help and exit.
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
BF_REPO_URL Repository URL (default: the upstream GitHub repository).
|
||||||
|
BF_INSTALL_DIR Checkout directory; same as --dir.
|
||||||
|
BF_BIN_DIR Tool link directory (default: /usr/local/bin).
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
* Installer prerequisites are git, curl and ansible-core (ansible-playbook).
|
||||||
|
* Supported baseline is Ubuntu 24.04 / 26.04; any other release only warns.
|
||||||
|
* This script installs code only. It never writes, fetches, or prints a
|
||||||
|
secret; device credentials are handled by admin/bf-creds.
|
||||||
|
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.
|
||||||
|
run() {
|
||||||
|
if [[ "$CHECK_MODE" -eq 1 ]]; then
|
||||||
|
printf 'bf-bootstrap: check: would run: %s\n' "$*"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--check) CHECK_MODE=1; shift ;;
|
||||||
|
--dir) DIR="${2:?missing value for --dir}"; shift 2 ;;
|
||||||
|
--branch) BRANCH="${2:?missing value for --branch}"; shift 2 ;;
|
||||||
|
--yes|-y) ASSUME_YES=1; shift ;;
|
||||||
|
--help|-h) usage; exit 0 ;;
|
||||||
|
*) printf 'bf-bootstrap: ERROR: unknown argument: %s\n\n' "$1" >&2; usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Where does this script live? When it runs from a checkout, that checkout is
|
||||||
|
# the default target; the piped one-command form has no checkout, so the
|
||||||
|
# default install directory is used.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
SCRIPT_SOURCE="${BASH_SOURCE[0]:-}"
|
||||||
|
if [[ -n "$SCRIPT_SOURCE" && -r "$SCRIPT_SOURCE" ]]; then
|
||||||
|
SCRIPT_REAL="$(readlink -f "$SCRIPT_SOURCE" 2>/dev/null || printf '%s' "$SCRIPT_SOURCE")"
|
||||||
|
CANDIDATE="$(cd "$(dirname "$SCRIPT_REAL")/.." 2>/dev/null && pwd || true)"
|
||||||
|
if [[ -n "$CANDIDATE" && -f "$CANDIDATE/admin/bf-bootstrap.sh" && -d "$CANDIDATE/scripts/diagnostics" ]]; then
|
||||||
|
CHECKOUT_ROOT="$CANDIDATE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -z "$DIR" ]]; then
|
||||||
|
DIR="${BF_INSTALL_DIR:-${CHECKOUT_ROOT:-$INSTALL_DIR_DEFAULT}}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$BRANCH" ]]; then
|
||||||
|
die 'branch must not be empty'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 1. Root check. --check only reports, so it works as an unprivileged user.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
if [[ "$CHECK_MODE" -eq 1 ]]; then
|
||||||
|
if [[ "$(id -u)" -ne 0 ]]; then
|
||||||
|
say 'check: not running as root; apply mode will require sudo'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ "$(id -u)" -ne 0 ]]; then
|
||||||
|
die "must run as root: sudo $0 (or: sudo bf-bootstrap.sh)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 2. Ubuntu release check. A different release is only a warning: the goal is
|
||||||
|
# to install the toolbox, not to block a working host.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
DISTRO_ID=""
|
||||||
|
DISTRO_VERSION=""
|
||||||
|
DISTRO_PRETTY="unknown"
|
||||||
|
if [[ -r /etc/os-release ]]; then
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. /etc/os-release
|
||||||
|
DISTRO_ID="${ID:-}"
|
||||||
|
DISTRO_VERSION="${VERSION_ID:-}"
|
||||||
|
DISTRO_PRETTY="${PRETTY_NAME:-unknown}"
|
||||||
|
fi
|
||||||
|
if [[ "$DISTRO_ID" == "ubuntu" ]]; then
|
||||||
|
SUPPORTED=0
|
||||||
|
for release in "${SUPPORTED_UBUNTU[@]}"; do
|
||||||
|
if [[ "$DISTRO_VERSION" == "$release" ]]; then
|
||||||
|
SUPPORTED=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "$SUPPORTED" -eq 1 ]]; then
|
||||||
|
say "ubuntu ${DISTRO_VERSION} is a supported baseline"
|
||||||
|
else
|
||||||
|
warn "Ubuntu ${DISTRO_VERSION:-unknown} is outside the tested baseline (${SUPPORTED_UBUNTU[*]}); continuing"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "this is not an Ubuntu host (ID=${DISTRO_ID:-unknown}, ${DISTRO_PRETTY}); the Field OS baseline is Ubuntu ${SUPPORTED_UBUNTU[*]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Operator confirmation. Only a terminal prompts: the documented one-command
|
||||||
|
# form (`curl ... | sudo bash`) has no terminal on stdin and never blocks.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
if [[ "$CHECK_MODE" -eq 0 && "$ASSUME_YES" -eq 0 && -t 0 ]]; then
|
||||||
|
printf 'Install or update the Blueforce toolbox in %s? [y/N] ' "$DIR"
|
||||||
|
ANSWER=""
|
||||||
|
read -r ANSWER || ANSWER=""
|
||||||
|
if [[ ! "$ANSWER" =~ ^[Yy]([Ee][Ss])?$ ]]; then
|
||||||
|
say 'aborted by the operator'
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 3. Dependencies.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
MISSING_COMMANDS=()
|
||||||
|
MISSING_PACKAGES=()
|
||||||
|
say 'dependency check'
|
||||||
|
for index in "${!DEPENDENCY_COMMANDS[@]}"; do
|
||||||
|
if ! command -v "${DEPENDENCY_COMMANDS[$index]}" >/dev/null 2>&1; then
|
||||||
|
MISSING_COMMANDS+=("${DEPENDENCY_COMMANDS[$index]}")
|
||||||
|
MISSING_PACKAGES+=("${DEPENDENCY_PACKAGES[$index]}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ ${#MISSING_PACKAGES[@]} -eq 0 ]]; then
|
||||||
|
say "dependencies present: ${DEPENDENCY_COMMANDS[*]}"
|
||||||
|
elif [[ "$CHECK_MODE" -eq 1 ]]; then
|
||||||
|
say "check: would install with apt-get: ${MISSING_PACKAGES[*]} (missing: ${MISSING_COMMANDS[*]})"
|
||||||
|
else
|
||||||
|
if ! command -v apt-get >/dev/null 2>&1; then
|
||||||
|
die "apt-get not found; install these packages first: ${MISSING_PACKAGES[*]}"
|
||||||
|
fi
|
||||||
|
say "installing dependencies: ${MISSING_PACKAGES[*]}"
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
run apt-get update
|
||||||
|
run apt-get install -y --no-install-recommends "${MISSING_PACKAGES[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 4. Repository: clone once, then fast-forward only.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
update_checkout() {
|
||||||
|
local remote="" origin_url="" dirty=""
|
||||||
|
if ! git -C "$DIR" rev-parse --verify --quiet HEAD >/dev/null 2>&1; then
|
||||||
|
warn "checkout has no commits yet: $DIR; skipping the update"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# Prefer `origin`, otherwise accept the checkout's single remote (a local
|
||||||
|
# clone of this project may name it `github`, as the upstream checkout does).
|
||||||
|
if git -C "$DIR" remote get-url origin >/dev/null 2>&1; then
|
||||||
|
remote="origin"
|
||||||
|
else
|
||||||
|
remote="$(git -C "$DIR" remote 2>/dev/null | head -n 1 || true)"
|
||||||
|
fi
|
||||||
|
if [[ -z "$remote" ]]; then
|
||||||
|
warn "checkout has no git remote: $DIR; skipping the update"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
origin_url="$(git -C "$DIR" remote get-url "$remote" 2>/dev/null || true)"
|
||||||
|
if [[ "$origin_url" != *"Blueforce-Linux"* ]]; then
|
||||||
|
warn "remote '$remote' looks unexpected: $origin_url"
|
||||||
|
fi
|
||||||
|
dirty="$(git -C "$DIR" status --porcelain 2>/dev/null || true)"
|
||||||
|
if [[ -n "$dirty" ]]; then
|
||||||
|
warn "local changes present in $DIR; skipping the update (nothing was reset or stashed)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$CHECK_MODE" -eq 1 ]]; then
|
||||||
|
say "check: would fast-forward $DIR to $remote/$BRANCH"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
say "updating $DIR (fast-forward only, remote $remote)"
|
||||||
|
if ! git -C "$DIR" fetch --prune --quiet "$remote" "$BRANCH"; then
|
||||||
|
warn "fetch failed (offline?); keeping the current checkout"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! git -C "$DIR" merge --ff-only --quiet "$remote/$BRANCH"; then
|
||||||
|
warn "cannot fast-forward to $remote/$BRANCH; keeping the current checkout (no reset performed)"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -e "$DIR" && ! -d "$DIR" ]]; then
|
||||||
|
die "install directory exists and is not a directory: $DIR"
|
||||||
|
fi
|
||||||
|
if [[ -d "$DIR" ]]; then
|
||||||
|
if [[ -d "$DIR/.git" ]]; then
|
||||||
|
say "existing checkout: $DIR"
|
||||||
|
update_checkout
|
||||||
|
else
|
||||||
|
die "directory exists but is not a git checkout; refusing to touch it: $DIR"
|
||||||
|
fi
|
||||||
|
elif [[ "$CHECK_MODE" -eq 1 ]]; then
|
||||||
|
say "check: would clone $REPO_URL (branch $BRANCH) into $DIR"
|
||||||
|
else
|
||||||
|
say "cloning $REPO_URL (branch $BRANCH) into $DIR"
|
||||||
|
run install -d -m 0755 "$(dirname "$DIR")"
|
||||||
|
run git clone --branch "$BRANCH" --single-branch -- "$REPO_URL" "$DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 5. Link the tools into /usr/local/bin. Idempotent: `ln -sfn` refreshes an
|
||||||
|
# existing link, and a re-run after `git pull` needs no extra step.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
link_tools() {
|
||||||
|
local source="" base=""
|
||||||
|
for source in "$DIR"/admin/bf-* "$DIR"/scripts/diagnostics/bf-* "$DIR"/scripts/maintenance/bf-*; do
|
||||||
|
if [[ ! -f "$source" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
base="$(basename "$source")"
|
||||||
|
if [[ "$CHECK_MODE" -eq 1 ]]; then
|
||||||
|
say "check: would link $source -> $BIN_DIR/$base"
|
||||||
|
else
|
||||||
|
chmod 0755 "$source"
|
||||||
|
ln -sfn "$source" "$BIN_DIR/$base"
|
||||||
|
fi
|
||||||
|
LINKED=$((LINKED + 1))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -d "$DIR" ]]; then
|
||||||
|
run install -d -m 0755 "$BIN_DIR"
|
||||||
|
link_tools
|
||||||
|
if [[ "$LINKED" -eq 0 ]]; then
|
||||||
|
warn "no bf-* tools found under $DIR (admin/, scripts/diagnostics/, scripts/maintenance/)"
|
||||||
|
elif [[ "$CHECK_MODE" -eq 0 ]]; then
|
||||||
|
say "linked $LINKED tools into $BIN_DIR"
|
||||||
|
fi
|
||||||
|
elif [[ "$CHECK_MODE" -eq 1 ]]; then
|
||||||
|
say "check: would link the bf-* tools from $DIR into $BIN_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 6. bf-menu: the on-device installer menu entry point.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
MENU_SOURCE="$DIR/admin/bf-menu"
|
||||||
|
if [[ -f "$MENU_SOURCE" ]]; then
|
||||||
|
if [[ "$CHECK_MODE" -eq 1 ]]; then
|
||||||
|
say "check: would make $MENU_SOURCE executable and link $BIN_DIR/bf-menu"
|
||||||
|
else
|
||||||
|
chmod 0755 "$MENU_SOURCE"
|
||||||
|
ln -sfn "$MENU_SOURCE" "$BIN_DIR/bf-menu"
|
||||||
|
say "bf-menu ready: $BIN_DIR/bf-menu"
|
||||||
|
fi
|
||||||
|
MENU_READY=1
|
||||||
|
else
|
||||||
|
warn "admin/bf-menu is not present in $DIR; 'sudo bf-menu' will work once the menu module is in the checkout"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 7. Summary.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
if [[ "$CHECK_MODE" -eq 1 ]]; then
|
||||||
|
printf '\nbf-bootstrap: check complete -- no changes were made.\n'
|
||||||
|
printf ' checkout : %s\n' "$DIR"
|
||||||
|
printf ' branch : %s\n' "$BRANCH"
|
||||||
|
printf ' tools : %s tool(s) would be linked into %s\n' "$LINKED" "$BIN_DIR"
|
||||||
|
printf ' next : sudo bf-bootstrap.sh --yes\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
REVISION=""
|
||||||
|
if [[ -d "$DIR/.git" ]]; then
|
||||||
|
REVISION="$(git -C "$DIR" rev-parse --short HEAD 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
printf '\nbf-bootstrap: summary\n'
|
||||||
|
printf ' repository : %s\n' "$DIR"
|
||||||
|
printf ' branch : %s\n' "$BRANCH"
|
||||||
|
if [[ -n "$REVISION" ]]; then
|
||||||
|
printf ' revision : %s\n' "$REVISION"
|
||||||
|
fi
|
||||||
|
printf ' tools : %s linked into %s\n' "$LINKED" "$BIN_DIR"
|
||||||
|
if [[ "$MENU_READY" -eq 1 ]]; then
|
||||||
|
printf ' bf-menu : %s\n' "$BIN_DIR/bf-menu"
|
||||||
|
else
|
||||||
|
printf ' bf-menu : pending (admin/bf-menu is not in the checkout yet)\n'
|
||||||
|
fi
|
||||||
|
printf ' next : sudo bf-menu\n'
|
||||||
|
printf '\nInstallation complete. To get started: sudo bf-menu\n'
|
||||||
|
printf 'Kurulum tamam. Başlamak için: sudo bf-menu\n'
|
||||||
236
admin/bf-creds
Executable file
236
admin/bf-creds
Executable file
|
|
@ -0,0 +1,236 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# bf-creds — optional, default-OFF credential generation and local delivery.
|
||||||
|
#
|
||||||
|
# Scope (deliberate, decided with the operator)
|
||||||
|
# * DEFAULT BEHAVIOUR: nothing is generated. Unless `--generate` (or
|
||||||
|
# BF_GENERATE_CREDENTIALS=1) is given, no password is created and the
|
||||||
|
# command says so explicitly.
|
||||||
|
# * With `--generate`: a device credential record is written to
|
||||||
|
# /root/blueforce-credentials.txt with mode 0600. It carries the device
|
||||||
|
# identity (BF-<no>), the hostname, the creation time, the WireGuard
|
||||||
|
# PUBLIC key plus the private key PATH (never its contents), the RustDesk
|
||||||
|
# ID when the installer recorded one, and a freshly generated local
|
||||||
|
# administrator password.
|
||||||
|
# * With `--fetch`: the record is copied to
|
||||||
|
# <repo>/admin/secrets/<dealer-number>.txt (directory 0700, file 0600).
|
||||||
|
# That path is git-ignored; it must never be committed.
|
||||||
|
# * Secrets are written to files only. No password is ever printed to
|
||||||
|
# stdout/stderr, written to a log, or passed through argv — so it cannot
|
||||||
|
# land in the shell history or the process list.
|
||||||
|
#
|
||||||
|
# What this tool does NOT do
|
||||||
|
# Remote access stays exactly as the installer left it: OpenSSH password
|
||||||
|
# authentication stays disabled (`PasswordAuthentication no`,
|
||||||
|
# docs/06-USERS-SSH-AND-PERMISSIONS.md, docs/11-SECURITY-HARDENING.md) and
|
||||||
|
# the `blueforce` maintenance account stays locked. bf-creds never edits
|
||||||
|
# sshd_config, never unlocks an account, and never enables a password login
|
||||||
|
# path; it is only an optional *local* delivery helper. See README.md
|
||||||
|
# ("admin/" section).
|
||||||
|
#
|
||||||
|
# Usage: sudo bf-creds [--generate] [--fetch] [--repo DIR] [--dealer-id N] [--help]
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SELF="${BASH_SOURCE[0]:-}"
|
||||||
|
SELF_REAL="$(readlink -f "$SELF" 2>/dev/null || printf '%s' "$SELF")"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$SELF_REAL")" && pwd)"
|
||||||
|
# shellcheck source=lib/creds.sh
|
||||||
|
source "$SCRIPT_DIR/lib/creds.sh"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
GENERATE=0
|
||||||
|
FETCH=0
|
||||||
|
REPO_OVERRIDE=""
|
||||||
|
DEALER_OVERRIDE=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: sudo bf-creds [--generate] [--fetch] [--repo DIR] [--dealer-id NNNNNNNN] [--help]
|
||||||
|
|
||||||
|
Report the device identity and the secrets that already exist on this host, and
|
||||||
|
optionally generate a local credential record and deliver it into the repository
|
||||||
|
checkout.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--generate Create the credential record (default path:
|
||||||
|
/root/blueforce-credentials.txt, mode 0600). Without this
|
||||||
|
flag NO password is generated and the command says so.
|
||||||
|
--fetch Copy the credential record to <repo>/admin/secrets/<dealer>.txt
|
||||||
|
(directory 0700, file 0600, git-ignored).
|
||||||
|
--repo DIR Repository root used as the --fetch destination
|
||||||
|
(default: the checkout this tool lives in).
|
||||||
|
--dealer-id N Eight-digit dealer number. Default: detected from the device
|
||||||
|
identity (installer state file, then the hostname).
|
||||||
|
--help, -h Show this help and exit.
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
BF_GENERATE_CREDENTIALS=1 Same as --generate (still opt-in; the default is off).
|
||||||
|
BF_CREDENTIALS_FILE=PATH Override the credential record path.
|
||||||
|
BF_WG_DIR=PATH Override the WireGuard directory.
|
||||||
|
BF_STATE_FILE=PATH Override the installer state file.
|
||||||
|
BF_RUSTDESK_ID_FILE=PATH Override the recorded RustDesk ID file.
|
||||||
|
|
||||||
|
Guarantees:
|
||||||
|
* The generated password is written to the record file only. It is never
|
||||||
|
printed, never logged, and never placed on a command line.
|
||||||
|
* The WireGuard private key is never read: only its path is reported.
|
||||||
|
* Remote access is not changed: SSH password authentication stays disabled
|
||||||
|
and the `blueforce` account stays locked.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--generate) GENERATE=1; shift ;;
|
||||||
|
--fetch) FETCH=1; shift ;;
|
||||||
|
--repo) REPO_OVERRIDE="${2:?missing value for --repo}"; shift 2 ;;
|
||||||
|
--dealer-id) DEALER_OVERRIDE="${2:?missing value for --dealer-id}"; shift 2 ;;
|
||||||
|
--help|-h) usage; exit 0 ;;
|
||||||
|
*) printf 'ERROR: unknown argument: %s\n\n' "$1" >&2; usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${BF_GENERATE_CREDENTIALS:-0}" == "1" ]]; then
|
||||||
|
GENERATE=1
|
||||||
|
fi
|
||||||
|
if [[ -n "$DEALER_OVERRIDE" ]] && [[ ! "$DEALER_OVERRIDE" =~ ^[0-9]{8}$ ]]; then
|
||||||
|
printf 'ERROR: --dealer-id must be exactly 8 digits\n' >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ -n "$REPO_OVERRIDE" ]]; then
|
||||||
|
if [[ ! -d "$REPO_OVERRIDE" ]]; then
|
||||||
|
printf 'ERROR: --repo must name an existing directory: %s\n' "$REPO_OVERRIDE" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
REPO_ROOT="$(cd "$REPO_OVERRIDE" && pwd)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
resolve_dealer_number() {
|
||||||
|
local id=""
|
||||||
|
if [[ -n "$DEALER_OVERRIDE" ]]; then
|
||||||
|
printf '%s' "$DEALER_OVERRIDE"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
id="$(bf_detect_device_id || true)"
|
||||||
|
bf_dealer_number "$id"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_report() {
|
||||||
|
local id="" dealer="" host="" wg_pub="" wg_path="" rustdesk=""
|
||||||
|
id="$(bf_detect_device_id || true)"
|
||||||
|
dealer="$(resolve_dealer_number || true)"
|
||||||
|
host="$(hostname 2>/dev/null || echo unknown)"
|
||||||
|
printf 'device\n'
|
||||||
|
printf ' device_id : %s\n' "${id:-unknown}"
|
||||||
|
printf ' dealer_number : %s\n' "${dealer:-unknown}"
|
||||||
|
printf ' hostname : %s\n' "$host"
|
||||||
|
printf 'wireguard\n'
|
||||||
|
if wg_pub="$(bf_wireguard_public_key)"; then
|
||||||
|
printf ' public_key : %s\n' "$wg_pub"
|
||||||
|
else
|
||||||
|
printf ' public_key : not present\n'
|
||||||
|
fi
|
||||||
|
if wg_path="$(bf_wireguard_private_key_path)"; then
|
||||||
|
printf ' private_key_path : %s (contents are never read or recorded)\n' "$wg_path"
|
||||||
|
else
|
||||||
|
printf ' private_key_path : not present\n'
|
||||||
|
fi
|
||||||
|
printf 'rustdesk\n'
|
||||||
|
if rustdesk="$(bf_rustdesk_id)"; then
|
||||||
|
printf ' id : %s\n' "$rustdesk"
|
||||||
|
else
|
||||||
|
printf ' id : not present\n'
|
||||||
|
fi
|
||||||
|
printf 'credential record\n'
|
||||||
|
printf ' path : %s\n' "$BF_CREDENTIALS_FILE"
|
||||||
|
if [[ -f "$BF_CREDENTIALS_FILE" ]]; then
|
||||||
|
printf ' present : yes (mode %s)\n' "$(bf_file_mode "$BF_CREDENTIALS_FILE")"
|
||||||
|
if bf_has_generated_password "$BF_CREDENTIALS_FILE"; then
|
||||||
|
printf ' administrator password recorded : yes (the value lives only in the file)\n'
|
||||||
|
else
|
||||||
|
printf ' administrator password recorded : no\n'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf ' present : no\n'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# credentials_body <timestamp> <password> (stdout = the record file body)
|
||||||
|
# The only caller pipes this straight into bf_write_secret_file; the body is
|
||||||
|
# never printed anywhere else.
|
||||||
|
credentials_body() {
|
||||||
|
local ts="$1" password="$2"
|
||||||
|
local id="" dealer="" host="" wg_pub="" wg_path="" rustdesk=""
|
||||||
|
id="$(bf_detect_device_id || true)"
|
||||||
|
dealer="$(resolve_dealer_number || true)"
|
||||||
|
host="$(hostname 2>/dev/null || echo unknown)"
|
||||||
|
wg_pub="$(bf_wireguard_public_key || true)"
|
||||||
|
wg_path="$(bf_wireguard_private_key_path || true)"
|
||||||
|
rustdesk="$(bf_rustdesk_id || true)"
|
||||||
|
cat <<EOF
|
||||||
|
# BLUEFORCE DEVICE CREDENTIALS
|
||||||
|
#
|
||||||
|
# Generated by bf-creds on the device. Root-owned, mode 0600.
|
||||||
|
# Delivery target (bf-creds --fetch): <checkout>/${BF_SECRETS_SUBDIR}/<dealer-number>.txt
|
||||||
|
# That path is git-ignored and must never be committed.
|
||||||
|
#
|
||||||
|
# Remote access is NOT changed by this tool: OpenSSH password authentication
|
||||||
|
# stays disabled (PasswordAuthentication no) and the blueforce maintenance
|
||||||
|
# account stays locked. See docs/06-USERS-SSH-AND-PERMISSIONS.md.
|
||||||
|
|
||||||
|
device_id: ${id:-unknown}
|
||||||
|
dealer_number: ${dealer:-unknown}
|
||||||
|
hostname: ${host}
|
||||||
|
generated_at: ${ts}
|
||||||
|
generator: bf-creds
|
||||||
|
|
||||||
|
[wireguard]
|
||||||
|
public_key: ${wg_pub:-not-present}
|
||||||
|
private_key_path: ${wg_path:-not-present}
|
||||||
|
private_key: NOT RECORDED (the WireGuard private key never leaves the device)
|
||||||
|
|
||||||
|
[rustdesk]
|
||||||
|
id: ${rustdesk:-not-present}
|
||||||
|
|
||||||
|
[generated]
|
||||||
|
local_administrator_password: ${password}
|
||||||
|
note: recorded for delivery only; bf-creds does not apply this password to any
|
||||||
|
account, does not edit sshd_config, and does not open a password login path.
|
||||||
|
Applying it stays a deliberate operator action on the device console.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
print_report
|
||||||
|
|
||||||
|
if [[ "$GENERATE" -eq 1 ]]; then
|
||||||
|
generated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unknown)"
|
||||||
|
password="$(bf_generate_secret)" || bf_die 'cannot generate a random secret (openssl and /dev/urandom are both unavailable)'
|
||||||
|
if ! credentials_body "$generated_at" "$password" | bf_write_secret_file "$BF_CREDENTIALS_FILE"; then
|
||||||
|
bf_die "could not write the credential record: $BF_CREDENTIALS_FILE"
|
||||||
|
fi
|
||||||
|
unset password
|
||||||
|
printf '\ncredential record written : %s (mode 600)\n' "$BF_CREDENTIALS_FILE"
|
||||||
|
printf ' the generated local administrator password exists in that file only;\n'
|
||||||
|
printf ' bf-creds does not apply it to an account, does not enable SSH password\n'
|
||||||
|
printf ' login, and leaves the blueforce account locked.\n'
|
||||||
|
else
|
||||||
|
printf '\ncredential generation : DISABLED (this is the default)\n'
|
||||||
|
printf ' no password was generated. Re-run with --generate (or BF_GENERATE_CREDENTIALS=1)\n'
|
||||||
|
printf ' to create %s.\n' "$BF_CREDENTIALS_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FETCH" -eq 1 ]]; then
|
||||||
|
if [[ ! -s "$BF_CREDENTIALS_FILE" ]]; then
|
||||||
|
bf_die "--fetch needs an existing credential record: $BF_CREDENTIALS_FILE (run --generate first)"
|
||||||
|
fi
|
||||||
|
dealer="$(resolve_dealer_number || true)"
|
||||||
|
if [[ -z "$dealer" ]]; then
|
||||||
|
bf_die 'cannot determine the dealer number; pass --dealer-id <8 digits> (fail-closed: the delivery file name depends on it)'
|
||||||
|
fi
|
||||||
|
destination="$(bf_secrets_target_path "$REPO_ROOT" "$dealer")" || bf_die 'cannot build the delivery path'
|
||||||
|
if ! bf_deliver_secret "$BF_CREDENTIALS_FILE" "$destination"; then
|
||||||
|
bf_die "could not deliver the credential record to: $destination"
|
||||||
|
fi
|
||||||
|
printf '\ncredential record delivered : %s (mode 600, directory 700, git-ignored)\n' "$destination"
|
||||||
|
fi
|
||||||
1626
admin/bf-menu
Executable file
1626
admin/bf-menu
Executable file
File diff suppressed because it is too large
Load diff
249
admin/lib/creds.sh
Executable file
249
admin/lib/creds.sh
Executable file
|
|
@ -0,0 +1,249 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# creds.sh — shared helpers for the Blueforce credential tooling (admin/bf-creds).
|
||||||
|
#
|
||||||
|
# Contract
|
||||||
|
# * No helper here ever writes a secret to stdout, stderr, or a log file.
|
||||||
|
# Secrets travel through files only: root-owned, mode 0600, inside a mode
|
||||||
|
# 0700 directory. Callers capture generated values with `$( ... )` (which
|
||||||
|
# never prints) and must never echo, printf, export, or log them.
|
||||||
|
# * Lookup helpers are read-only. Only the `bf_write_secret_file` /
|
||||||
|
# `bf_deliver_secret` helpers touch the filesystem, and only at the target
|
||||||
|
# the caller names explicitly.
|
||||||
|
# * A WireGuard *private key is never read* by this library: only its path is
|
||||||
|
# ever reported. The same rule applies to every other private key material.
|
||||||
|
# * This library does not change the remote-access decision recorded in
|
||||||
|
# docs/06-USERS-SSH-AND-PERMISSIONS.md: OpenSSH password authentication
|
||||||
|
# stays disabled (`PasswordAuthentication no`) and the `blueforce`
|
||||||
|
# maintenance account stays locked. bf-creds is an optional *local*
|
||||||
|
# delivery helper: it never edits sshd_config, never unlocks an account,
|
||||||
|
# and never opens a password login path. See the header of admin/bf-creds
|
||||||
|
# and the "admin/" section of README.md.
|
||||||
|
#
|
||||||
|
# Usage: sourced by admin/bf-creds. Not meant to be executed on its own.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Executing the library directly would silently do nothing useful; say so.
|
||||||
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
|
printf 'creds.sh is a library: source it (see admin/bf-creds).\n' >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${BF_CREDS_LIB_LOADED:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
BF_CREDS_LIB_LOADED=1
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Locations. Every path is overridable so tests can point the helpers at a
|
||||||
|
# sandbox and so a site can relocate the credential record deliberately.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
: "${BF_STATE_FILE:=/var/lib/blueforce/state.json}"
|
||||||
|
: "${BF_WG_DIR:=/etc/wireguard}"
|
||||||
|
: "${BF_RUSTDESK_ID_FILE:=/var/lib/blueforce/rustdesk-id}"
|
||||||
|
: "${BF_CREDENTIALS_FILE:=/root/blueforce-credentials.txt}"
|
||||||
|
: "${BF_SECRETS_SUBDIR:=admin/secrets}"
|
||||||
|
: "${BF_CREDENTIALS_MODE:=600}"
|
||||||
|
: "${BF_SECRETS_DIR_MODE:=700}"
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Logging. Messages are always descriptions, never values.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
bf_log() { printf '[bf-creds] %s\n' "$*" >&2; }
|
||||||
|
bf_warn() { printf '[bf-creds] WARN: %s\n' "$*" >&2; }
|
||||||
|
bf_die() { printf '[bf-creds] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Read-only identity helpers.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
# bf_detect_device_id -> prints BF-<8 digits> (BF_DEVICE, then the installer
|
||||||
|
# state file, then the hostname); prints nothing and returns 1 when unknown.
|
||||||
|
bf_detect_device_id() {
|
||||||
|
local id=""
|
||||||
|
if [[ "${BF_DEVICE:-}" =~ ^BF-[0-9]{8}$ ]]; then
|
||||||
|
printf '%s' "$BF_DEVICE"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ -r "$BF_STATE_FILE" ]] && grep -q 'device_id' "$BF_STATE_FILE" 2>/dev/null; then
|
||||||
|
id="$(grep -oE 'BF-[0-9]{8}' "$BF_STATE_FILE" 2>/dev/null | head -n 1 || true)"
|
||||||
|
fi
|
||||||
|
if [[ -z "$id" ]]; then
|
||||||
|
id="$(hostname 2>/dev/null | tr '[:lower:]' '[:upper:]' || true)"
|
||||||
|
fi
|
||||||
|
if [[ "$id" =~ ^BF-[0-9]{8}$ ]]; then
|
||||||
|
printf '%s' "$id"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_dealer_number <device-id> -> prints the 8-digit dealer number, else nothing.
|
||||||
|
bf_dealer_number() {
|
||||||
|
local id="${1:-}"
|
||||||
|
if [[ "$id" =~ ^BF-([0-9]{8})$ ]]; then
|
||||||
|
printf '%s' "${BASH_REMATCH[1]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_wireguard_public_key -> prints the public key (safe to display); nothing
|
||||||
|
# and return 1 when the installer has not created a keypair yet.
|
||||||
|
bf_wireguard_public_key() {
|
||||||
|
local f="$BF_WG_DIR/publickey" key=""
|
||||||
|
if [[ -s "$f" ]]; then
|
||||||
|
key="$(tr -d '[:space:]' < "$f" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
if [[ -n "$key" ]]; then
|
||||||
|
printf '%s' "$key"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_wireguard_private_key_path -> prints the PATH of the private key only.
|
||||||
|
# The contents are never opened: only the file's existence is checked.
|
||||||
|
bf_wireguard_private_key_path() {
|
||||||
|
local f="$BF_WG_DIR/privatekey"
|
||||||
|
if [[ -e "$f" ]]; then
|
||||||
|
printf '%s' "$f"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_rustdesk_id -> prints the RustDesk peer ID recorded by module 09-rustdesk.
|
||||||
|
bf_rustdesk_id() {
|
||||||
|
local f="$BF_RUSTDESK_ID_FILE" id=""
|
||||||
|
if [[ -s "$f" ]]; then
|
||||||
|
id="$(tr -d '[:space:]' < "$f" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
if [[ "$id" =~ ^[0-9]+$ ]]; then
|
||||||
|
printf '%s' "$id"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Secret generation and file handling.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
# bf_generate_secret -> prints a fresh random secret on stdout (openssl, then
|
||||||
|
# /dev/urandom as a fallback). Capturing the result with `$( ... )` never
|
||||||
|
# prints it; the value must reach a mode-0600 file and nothing else. It is
|
||||||
|
# deliberately never passed through argv or a command line.
|
||||||
|
bf_generate_secret() {
|
||||||
|
local secret=""
|
||||||
|
if command -v openssl >/dev/null 2>&1; then
|
||||||
|
secret="$(openssl rand -base64 24 2>/dev/null | tr -d '\n' || true)"
|
||||||
|
fi
|
||||||
|
if [[ -z "$secret" ]] && [[ -r /dev/urandom ]] && command -v base64 >/dev/null 2>&1; then
|
||||||
|
secret="$(head -c 32 /dev/urandom 2>/dev/null | base64 | tr -d '\n' || true)"
|
||||||
|
fi
|
||||||
|
if [[ -z "$secret" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
printf '%s' "$secret"
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_file_mode <path> -> prints the octal mode (Linux stat).
|
||||||
|
bf_file_mode() { stat -c '%a' "$1" 2>/dev/null || true; }
|
||||||
|
|
||||||
|
# bf_has_generated_password <file> -> 0 when the record carries a password line.
|
||||||
|
bf_has_generated_password() {
|
||||||
|
local f="${1:-}"
|
||||||
|
if [[ -s "$f" ]] && grep -qE '^local_administrator_password:[[:space:]]*[^[:space:]]+' "$f"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_write_secret_file <dest> (stdin: entire file body)
|
||||||
|
# Writes stdin to <dest> atomically, forces mode 0600 on the file and 0700 on
|
||||||
|
# the parent directory, and verifies the result. Never echoes the body.
|
||||||
|
bf_write_secret_file() {
|
||||||
|
local dest="${1:-}" tmp=""
|
||||||
|
if [[ -z "$dest" ]]; then
|
||||||
|
bf_warn 'bf_write_secret_file: destination required'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ -L "$dest" ]]; then
|
||||||
|
bf_warn "refusing to write through a symlink: $dest"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
tmp="$(mktemp "${TMPDIR:-/tmp}/bf-creds.XXXXXX" 2>/dev/null || true)"
|
||||||
|
if [[ -z "$tmp" ]]; then
|
||||||
|
bf_warn 'cannot create a temporary staging file'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
chmod 600 "$tmp" || { rm -f "$tmp"; return 1; }
|
||||||
|
if ! cat > "$tmp"; then
|
||||||
|
rm -f "$tmp"
|
||||||
|
bf_warn 'cannot stage the credential body'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! install -d -m "$BF_SECRETS_DIR_MODE" -- "$(dirname "$dest")"; then
|
||||||
|
rm -f "$tmp"
|
||||||
|
bf_warn "cannot create the credential directory: $(dirname "$dest")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! install -m "$BF_CREDENTIALS_MODE" -- "$tmp" "$dest"; then
|
||||||
|
rm -f "$tmp"
|
||||||
|
bf_warn "cannot write the credential file: $dest"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rm -f "$tmp"
|
||||||
|
if [[ "$(bf_file_mode "$dest")" != "$BF_CREDENTIALS_MODE" ]]; then
|
||||||
|
bf_warn "credential file mode is not $BF_CREDENTIALS_MODE: $dest"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Delivery.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
# bf_secrets_target_path <repo-root> <dealer-number> -> destination path.
|
||||||
|
bf_secrets_target_path() {
|
||||||
|
local repo_root="${1:-}" dealer="${2:-}"
|
||||||
|
if [[ -z "$repo_root" || -z "$dealer" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
printf '%s/%s/%s.txt' "$repo_root" "$BF_SECRETS_SUBDIR" "$dealer"
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_deliver_secret <src> <dest>
|
||||||
|
# Copies a credential file to the delivery target: directory mode 0700, file
|
||||||
|
# mode 0600. Prints nothing; the caller reports the path only.
|
||||||
|
bf_deliver_secret() {
|
||||||
|
local src="${1:-}" dest="${2:-}"
|
||||||
|
if [[ ! -s "$src" ]]; then
|
||||||
|
bf_warn "credential file is missing or empty: $src"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ -z "$dest" ]]; then
|
||||||
|
bf_warn 'bf_deliver_secret: destination required'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ -L "$dest" ]]; then
|
||||||
|
bf_warn "refusing to write through a symlink: $dest"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! install -d -m "$BF_SECRETS_DIR_MODE" -- "$(dirname "$dest")"; then
|
||||||
|
bf_warn "cannot create the delivery directory: $(dirname "$dest")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! install -m "$BF_CREDENTIALS_MODE" -- "$src" "$dest"; then
|
||||||
|
bf_warn "cannot deliver the credential file to: $dest"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "$(bf_file_mode "$dest")" != "$BF_CREDENTIALS_MODE" ]]; then
|
||||||
|
bf_warn "delivered file mode is not $BF_CREDENTIALS_MODE: $dest"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
476
admin/lib/targets.sh
Normal file
476
admin/lib/targets.sh
Normal file
|
|
@ -0,0 +1,476 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# admin/lib/targets.sh -- device identity, inventory parsing and target selection.
|
||||||
|
#
|
||||||
|
# Sourced by admin/bf-menu. Contains no UI rendering and no execution: it answers
|
||||||
|
# "which hosts does this operation apply to, and which Ansible --limit does that
|
||||||
|
# become".
|
||||||
|
#
|
||||||
|
# Contracts it enforces (sources of truth in parentheses):
|
||||||
|
# * dealer number matches ^[0-9]{8}$ exactly (docs/02)
|
||||||
|
# * machine hostname is bf-<dealer>, human id is BF-<dealer> (docs/02)
|
||||||
|
# * waves are lab(2) -> pilot_1(5) -> pilot_2(20) -> wave_1(50) -> wave_2(100)
|
||||||
|
# -> production, in that order (docs/10)
|
||||||
|
# * the inventory is the Ansible inventory; no device is ever addressed that is
|
||||||
|
# not in it (docs/09)
|
||||||
|
#
|
||||||
|
# Inputs (set by bf-menu before calling):
|
||||||
|
# BF_REPO repository root
|
||||||
|
# BF_INVENTORY resolved inventory path, or empty to trigger auto-discovery
|
||||||
|
#
|
||||||
|
# shellcheck shell=bash
|
||||||
|
|
||||||
|
# Canonical wave order. Kept in one place so the menu, the gate checks and the
|
||||||
|
# fleet-wide loop can never disagree.
|
||||||
|
BF_WAVES=(lab pilot_1 pilot_2 wave_1 wave_2 production)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Identity helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# BF_DEALER_RE -- the single validation rule for dealer numbers.
|
||||||
|
BF_DEALER_RE='^[0-9]{8}$'
|
||||||
|
|
||||||
|
bf_dealer_validate() {
|
||||||
|
[[ "$1" =~ $BF_DEALER_RE ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
bf_dealer_to_host() {
|
||||||
|
printf 'bf-%s\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
bf_dealer_to_device_id() {
|
||||||
|
printf 'BF-%s\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_host_to_dealer HOST -> dealer number, empty when the name is not bf-<8 digits>.
|
||||||
|
bf_host_to_dealer() {
|
||||||
|
local host="$1"
|
||||||
|
if [[ "$host" =~ ^bf-([0-9]{8})$ ]]; then
|
||||||
|
printf '%s\n' "${BASH_REMATCH[1]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_is_wave NAME -- true for the six approved wave groups (docs/10).
|
||||||
|
bf_is_wave() {
|
||||||
|
local name="$1" wave
|
||||||
|
for wave in "${BF_WAVES[@]}"; do
|
||||||
|
[[ "$name" == "$wave" ]] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
bf_waves_csv() {
|
||||||
|
local IFS=,
|
||||||
|
printf '%s\n' "${BF_WAVES[*]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Inventory discovery and parsing
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# bf_inventory_candidates -- every path we are willing to read, best first.
|
||||||
|
# hosts.example.yml is listed last on purpose: it is a template with example
|
||||||
|
# device IDs, so it is only usable after an explicit acknowledgement.
|
||||||
|
bf_inventory_candidates() {
|
||||||
|
local dir="${BF_REPO:-.}/ansible/inventory"
|
||||||
|
if [[ -n "${BF_INVENTORY:-}" ]]; then
|
||||||
|
printf '%s\n' "$BF_INVENTORY"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf '%s\n' \
|
||||||
|
"$dir/hosts.yml" \
|
||||||
|
"$dir/hosts.yaml" \
|
||||||
|
"$dir/hosts" \
|
||||||
|
"$dir/hosts.ini"
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_inventory_resolve -- pick the inventory file. Prints the path on success.
|
||||||
|
# Prints a remediation message to stderr and returns 1 when nothing is usable.
|
||||||
|
bf_inventory_resolve() {
|
||||||
|
local candidate example="${BF_REPO:-.}/ansible/inventory/hosts.example.yml"
|
||||||
|
if [[ -n "${BF_INVENTORY:-}" ]]; then
|
||||||
|
if [[ -r "$BF_INVENTORY" ]]; then
|
||||||
|
printf '%s\n' "$BF_INVENTORY"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
tui_error "Inventory not readable: $BF_INVENTORY"
|
||||||
|
tui_note "Pass a real path with --inventory PATH or unset it to auto-discover." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
while IFS= read -r candidate; do
|
||||||
|
[[ -n "$candidate" ]] || continue
|
||||||
|
if [[ -r "$candidate" ]]; then
|
||||||
|
printf '%s\n' "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done < <(bf_inventory_candidates)
|
||||||
|
|
||||||
|
tui_error "No Ansible inventory found under ${BF_REPO:-.}/ansible/inventory/."
|
||||||
|
# These remediation lines go to stderr on purpose: the resolver's stdout is
|
||||||
|
# captured as the resolved path by the caller.
|
||||||
|
if [[ -r "$example" ]]; then
|
||||||
|
tui_note "Only the example inventory exists: $example" >&2
|
||||||
|
tui_note "It carries example device IDs (bf-1201xxxx) and must not be used against real devices." >&2
|
||||||
|
tui_note "Create the real inventory first:" >&2
|
||||||
|
tui_note " cp $example ${BF_REPO:-.}/ansible/inventory/hosts.yml" >&2
|
||||||
|
tui_note " \$EDITOR ${BF_REPO:-.}/ansible/inventory/hosts.yml" >&2
|
||||||
|
else
|
||||||
|
tui_note "Create ansible/inventory/hosts.yml with your bf-<8-digit dealer> hosts (docs/02, docs/09)." >&2
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_inventory_pairs FILE -- "group<TAB>host" for every host in the inventory.
|
||||||
|
# A deliberately small YAML reader: the inventory is a nested map of
|
||||||
|
# all.children.<group>.children.<wave>.hosts.<hostname>. It tracks indentation
|
||||||
|
# instead of pulling in a YAML parser (none is guaranteed on a field console).
|
||||||
|
bf_inventory_pairs() {
|
||||||
|
local inv="$1"
|
||||||
|
[[ -r "$inv" ]] || return 1
|
||||||
|
awk '
|
||||||
|
function trim(s) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", s); return s }
|
||||||
|
function unquote(s) { gsub(/^["\x27]|["\x27]$/, "", s); return s }
|
||||||
|
{
|
||||||
|
line = $0
|
||||||
|
if (line ~ /^[[:space:]]*#/) next
|
||||||
|
if (line ~ /^[[:space:]]*$/) next
|
||||||
|
if (line ~ /^[[:space:]]*-/) next
|
||||||
|
if (line !~ /:/) next
|
||||||
|
|
||||||
|
match(line, /^ */); ind = RLENGTH
|
||||||
|
key = line; sub(/^[[:space:]]*/, "", key); sub(/:.*$/, "", key)
|
||||||
|
key = unquote(trim(key))
|
||||||
|
val = line; sub(/^[^:]*:/, "", val); val = trim(val)
|
||||||
|
if (key == "") next
|
||||||
|
|
||||||
|
while (depth > 0 && sind[depth] >= ind) depth--
|
||||||
|
|
||||||
|
# A "hosts:" block owns the nearest enclosing group key.
|
||||||
|
if (key == "hosts" && val == "") {
|
||||||
|
h_active = 1; h_ind = ind; h_step = 0
|
||||||
|
h_grp = (depth > 0) ? skey[depth] : ""
|
||||||
|
skey[++depth] = key; sind[depth] = ind
|
||||||
|
next
|
||||||
|
}
|
||||||
|
# Anything at or above the "hosts:" indentation ends that block.
|
||||||
|
if (h_active && ind <= h_ind) h_active = 0
|
||||||
|
|
||||||
|
if (h_active && ind > h_ind) {
|
||||||
|
if (h_step == 0) h_step = ind
|
||||||
|
if (ind == h_step) {
|
||||||
|
print h_grp "\t" key
|
||||||
|
skey[++depth] = key; sind[depth] = ind
|
||||||
|
next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (val == "") { skey[++depth] = key; sind[depth] = ind }
|
||||||
|
}
|
||||||
|
' "$inv"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reads the inventory and caches the pairs; repeated calls are cheap and the
|
||||||
|
# cache is invalidated by BF_INVENTORY_CACHE_FILE going away.
|
||||||
|
bf_inventory_cache() {
|
||||||
|
local inv="${BF_INVENTORY:-}"
|
||||||
|
[[ -n "$inv" ]] || return 1
|
||||||
|
if [[ -z "${BF_INVENTORY_CACHE_FILE:-}" ]]; then
|
||||||
|
BF_INVENTORY_CACHE_FILE="$(mktemp "${TMPDIR:-/tmp}/bf-inventory.XXXXXX")"
|
||||||
|
export BF_INVENTORY_CACHE_FILE
|
||||||
|
fi
|
||||||
|
if [[ ! -s "$BF_INVENTORY_CACHE_FILE" ]]; then
|
||||||
|
bf_inventory_pairs "$inv" >"$BF_INVENTORY_CACHE_FILE" || return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
bf_inventory_groups() {
|
||||||
|
bf_inventory_cache || return 1
|
||||||
|
awk -F '\t' 'NF == 2 && !seen[$1]++ { print $1 }' "$BF_INVENTORY_CACHE_FILE" | grep -v '^$' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
bf_inventory_hosts() {
|
||||||
|
bf_inventory_cache || return 1
|
||||||
|
awk -F '\t' 'NF == 2 && !seen[$2]++ { print $2 }' "$BF_INVENTORY_CACHE_FILE" | grep -v '^$' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
bf_inventory_hosts_of_group() {
|
||||||
|
local group="$1"
|
||||||
|
bf_inventory_cache || return 1
|
||||||
|
awk -F '\t' -v g="$group" '$1 == g { print $2 }' "$BF_INVENTORY_CACHE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_count_lines -- wc -l without the set -e trap of grep -c returning 1 on zero.
|
||||||
|
bf_count_lines() {
|
||||||
|
local n
|
||||||
|
n="$(printf '%s\n' "$1" | grep -c . || true)"
|
||||||
|
printf '%s\n' "${n:-0}"
|
||||||
|
}
|
||||||
|
|
||||||
|
bf_inventory_group_exists() {
|
||||||
|
local group="$1"
|
||||||
|
bf_inventory_cache || return 1
|
||||||
|
awk -F '\t' -v g="$group" '$1 == g { found = 1; exit } END { exit !found }' "$BF_INVENTORY_CACHE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
bf_inventory_host_exists() {
|
||||||
|
local host="$1"
|
||||||
|
bf_inventory_cache || return 1
|
||||||
|
awk -F '\t' -v h="$host" '$2 == h { found = 1; exit } END { exit !found }' "$BF_INVENTORY_CACHE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
bf_inventory_count_all() {
|
||||||
|
bf_count_lines "$(bf_inventory_hosts || true)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_inventory_host_wave HOST -- the approved wave that contains HOST.
|
||||||
|
# Prints nothing and returns 1 when the device sits outside every wave group.
|
||||||
|
bf_inventory_host_wave() {
|
||||||
|
local host="$1" wave
|
||||||
|
bf_inventory_cache || return 1
|
||||||
|
for wave in "${BF_WAVES[@]}"; do
|
||||||
|
bf_inventory_group_exists "$wave" || continue
|
||||||
|
if bf_inventory_hosts_of_group "$wave" | grep -qx -- "$host"; then
|
||||||
|
printf '%s\n' "$wave"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_wave_groups_present -- canonical waves that actually exist in the inventory,
|
||||||
|
# always in rollout order.
|
||||||
|
bf_wave_groups_present() {
|
||||||
|
local wave
|
||||||
|
for wave in "${BF_WAVES[@]}"; do
|
||||||
|
if bf_inventory_group_exists "$wave"; then
|
||||||
|
printf '%s\n' "$wave"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Target selection
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
bf_target_reset() {
|
||||||
|
BF_TARGET_KIND=""
|
||||||
|
BF_TARGET_LIMIT=""
|
||||||
|
BF_TARGET_WAVE=""
|
||||||
|
BF_TARGET_LABEL=""
|
||||||
|
BF_TARGET_COUNT=0
|
||||||
|
}
|
||||||
|
|
||||||
|
bf_target_is_set() {
|
||||||
|
[[ -n "${BF_TARGET_KIND:-}" && -n "${BF_TARGET_LIMIT:-}" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_target_set_host DEALER [MODE]
|
||||||
|
# MODE: any|wave|release. Returns 1 (after explaining) when the mode forbids a
|
||||||
|
# single-device selection or the device is unknown to the inventory.
|
||||||
|
bf_target_set_host() {
|
||||||
|
local dealer="$1" mode="${2:-any}" host wave
|
||||||
|
if ! bf_dealer_validate "$dealer"; then
|
||||||
|
tui_error "Invalid dealer number '$dealer': it must match ${BF_DEALER_RE} (8 digits, no spaces)."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "$mode" == "release" ]]; then
|
||||||
|
tui_error "Release-gated operations are wave-scoped (docs/10): select a wave group, not a single device."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
host="$(bf_dealer_to_host "$dealer")"
|
||||||
|
if ! bf_inventory_host_exists "$host"; then
|
||||||
|
tui_error "$host is not in the inventory (${BF_INVENTORY:-unresolved})."
|
||||||
|
tui_note "Add the device to its wave group first, then retry (see admin/README.md, docs/02)."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
wave="$(bf_inventory_host_wave "$host" || true)"
|
||||||
|
if [[ -z "$wave" && "$mode" != "any" ]]; then
|
||||||
|
tui_error "$host is not inside an approved wave group ($(bf_waves_csv))."
|
||||||
|
tui_note "Wave-scoped playbooks refuse a device that no wave covers (fail-closed)."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
BF_TARGET_KIND="host"
|
||||||
|
BF_TARGET_LIMIT="$host"
|
||||||
|
BF_TARGET_WAVE="$wave"
|
||||||
|
BF_TARGET_COUNT=1
|
||||||
|
if [[ -n "$wave" ]]; then
|
||||||
|
BF_TARGET_LABEL="device $(bf_dealer_to_device_id "$dealer") ($host) in wave '$wave'"
|
||||||
|
else
|
||||||
|
BF_TARGET_LABEL="device $(bf_dealer_to_device_id "$dealer") ($host)"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_target_set_group GROUP [MODE]
|
||||||
|
bf_target_set_group() {
|
||||||
|
local group="$1" mode="${2:-any}" count
|
||||||
|
if [[ "$group" == "all" ]]; then
|
||||||
|
bf_target_set_all "$mode"
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
if ! bf_inventory_group_exists "$group"; then
|
||||||
|
tui_error "Group '$group' is not in the inventory (${BF_INVENTORY:-unresolved})."
|
||||||
|
tui_note "Known groups: $(bf_inventory_groups | tr '\n' ' ')"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "$mode" == "wave" || "$mode" == "release" ]] && ! bf_is_wave "$group"; then
|
||||||
|
tui_error "Group '$group' is not an approved wave; wave-scoped operations need one of: $(bf_waves_csv)."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
count="$(bf_count_lines "$(bf_inventory_hosts_of_group "$group" || true)")"
|
||||||
|
if [[ "$count" -eq 0 ]]; then
|
||||||
|
tui_error "Group '$group' contains no hosts; refusing an empty target."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
BF_TARGET_KIND="group"
|
||||||
|
BF_TARGET_LIMIT="$group"
|
||||||
|
BF_TARGET_WAVE="$group"
|
||||||
|
# shellcheck disable=SC2034 # read by bf-menu (library contract)
|
||||||
|
BF_TARGET_COUNT="$count"
|
||||||
|
if bf_is_wave "$group"; then
|
||||||
|
BF_TARGET_LABEL="wave '$group' ($count device(s))"
|
||||||
|
else
|
||||||
|
BF_TARGET_LABEL="group '$group' ($count device(s))"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_target_set_all MODE -- the whole fleet.
|
||||||
|
# For release-gated operations this is refused: the update/reboot playbooks are
|
||||||
|
# gated wave by wave and a fleet-wide limit would defeat the gate (docs/10).
|
||||||
|
bf_target_set_all() {
|
||||||
|
local mode="${1:-any}" count
|
||||||
|
if [[ "$mode" == "release" ]]; then
|
||||||
|
tui_error "Fleet-wide scope is refused for release-gated operations: roll out one wave at a time (docs/10)."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
count="$(bf_inventory_count_all)"
|
||||||
|
if [[ "$count" -eq 0 ]]; then
|
||||||
|
tui_error "The inventory lists no hosts; refusing an empty target."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
BF_TARGET_KIND="all"
|
||||||
|
BF_TARGET_LIMIT="all"
|
||||||
|
BF_TARGET_WAVE=""
|
||||||
|
# shellcheck disable=SC2034 # read by bf-menu (library contract)
|
||||||
|
BF_TARGET_COUNT="$count"
|
||||||
|
BF_TARGET_LABEL="all devices in the fleet ($count device(s))"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_target_parse WORD MODE -- accept what the operator typed in one prompt:
|
||||||
|
# an 8-digit dealer number, a group/wave name, or "all".
|
||||||
|
bf_target_parse() {
|
||||||
|
local word="$1" mode="${2:-any}"
|
||||||
|
bf_target_reset
|
||||||
|
if [[ -z "$word" ]]; then
|
||||||
|
tui_error "No target given."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "$word" == "all" ]]; then
|
||||||
|
bf_target_set_all "$mode"
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
if bf_dealer_validate "$word"; then
|
||||||
|
bf_target_set_host "$word" "$mode"
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
if bf_inventory_group_exists "$word"; then
|
||||||
|
bf_target_set_group "$word" "$mode"
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
# A bf-<dealer> hostname is accepted as a convenience.
|
||||||
|
if [[ "$word" =~ ^bf-[0-9]{8}$ ]]; then
|
||||||
|
if bf_dealer_validate "${word#bf-}"; then
|
||||||
|
bf_target_set_host "${word#bf-}" "$mode"
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
tui_error "'$word' is not a dealer number (${BF_DEALER_RE}), a group, or 'all'."
|
||||||
|
tui_note "Known groups: $(bf_inventory_groups | tr '\n' ' ')"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_target_describe -- one line for the confirmation screen.
|
||||||
|
bf_target_describe() {
|
||||||
|
printf '%s --limit %s%s\n' "${BF_TARGET_LABEL:-unset}" "${BF_TARGET_LIMIT:-unset}" \
|
||||||
|
"$([[ -n "${BF_TARGET_WAVE:-}" ]] && printf ' (wave %s)' "$BF_TARGET_WAVE")"
|
||||||
|
}
|
||||||
|
|
||||||
|
# bf_target_select MODE
|
||||||
|
# Interactive target picker. MODE:
|
||||||
|
# any single device, any group, or the whole fleet
|
||||||
|
# wave single device, approved wave group, or the whole fleet wave by wave
|
||||||
|
# release approved wave group only
|
||||||
|
# On success the BF_TARGET_* globals describe the selection.
|
||||||
|
bf_target_select() {
|
||||||
|
local mode="${1:-any}" choice word count index group_list=()
|
||||||
|
bf_target_reset
|
||||||
|
while true; do
|
||||||
|
tui_section "Target selection (mode: $mode)"
|
||||||
|
tui_item 1 "Single device by dealer number" "8 digits, e.g. 12010101"
|
||||||
|
tui_item 2 "Group / wave" "lab, pilot_1, pilot_2, wave_1, wave_2, production"
|
||||||
|
if [[ "$mode" == "release" ]]; then
|
||||||
|
tui_note " all) not offered: release-gated operations run one wave at a time"
|
||||||
|
else
|
||||||
|
tui_item 3 "All devices in the fleet" "wave-scoped playbooks ask per wave"
|
||||||
|
fi
|
||||||
|
tui_item 9 "Type the target directly" "dealer number, group or 'all'"
|
||||||
|
tui_footer "Back to the operation"
|
||||||
|
printf '\n'
|
||||||
|
if ! tui_read "Target > "; then return 1; fi
|
||||||
|
choice="$TUI_REPLY"
|
||||||
|
case "$choice" in
|
||||||
|
""|0|q|Q) return 1 ;;
|
||||||
|
1)
|
||||||
|
tui_read "Dealer number (8 digits) > " || return 1
|
||||||
|
word="$TUI_REPLY"
|
||||||
|
bf_target_set_host "$word" "$mode" && return 0
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
tui_section "Groups in the inventory"
|
||||||
|
group_list=()
|
||||||
|
while IFS= read -r group; do
|
||||||
|
[[ -n "$group" ]] || continue
|
||||||
|
group_list+=("$group")
|
||||||
|
done < <(if [[ "$mode" == "any" ]]; then bf_inventory_groups || true; else bf_wave_groups_present || true; fi)
|
||||||
|
if [[ ${#group_list[@]} -eq 0 ]]; then
|
||||||
|
tui_error "No usable group in the inventory (${BF_INVENTORY:-unresolved})."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
index=0
|
||||||
|
for group in "${group_list[@]}"; do
|
||||||
|
index=$((index + 1))
|
||||||
|
count="$(bf_count_lines "$(bf_inventory_hosts_of_group "$group" || true)")"
|
||||||
|
if bf_is_wave "$group"; then
|
||||||
|
tui_item "$index" "$group" "$count device(s), approved wave"
|
||||||
|
else
|
||||||
|
tui_item "$index" "$group" "$count device(s)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
printf '\n'
|
||||||
|
tui_note "Approved waves: $(bf_waves_csv) -- source: $(basename "${BF_INVENTORY:-unresolved}")"
|
||||||
|
if ! tui_read "Group number > "; then return 1; fi
|
||||||
|
choice="$TUI_REPLY"
|
||||||
|
if ! tui_menu_index "$choice" "$index"; then
|
||||||
|
tui_error "Enter a number between 1 and $index."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
bf_target_set_group "${group_list[$((choice - 1))]}" "$mode" && return 0
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
if [[ "$mode" == "release" ]]; then
|
||||||
|
tui_error "Fleet-wide scope is refused here."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
bf_target_set_all "$mode" && return 0
|
||||||
|
;;
|
||||||
|
9)
|
||||||
|
tui_read "Target (dealer number, group or all) > " || return 1
|
||||||
|
bf_target_parse "$TUI_REPLY" "$mode" && return 0
|
||||||
|
;;
|
||||||
|
*) tui_error "Unknown choice '$choice'." ;;
|
||||||
|
esac
|
||||||
|
printf '\n'
|
||||||
|
tui_pause
|
||||||
|
done
|
||||||
|
}
|
||||||
216
admin/lib/tui.sh
Normal file
216
admin/lib/tui.sh
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# admin/lib/tui.sh -- terminal UI helpers for the Blueforce operator console.
|
||||||
|
#
|
||||||
|
# Sourced by admin/bf-menu. This file contains presentation and input helpers
|
||||||
|
# only: no fleet logic, no inventory parsing, and no side effects when sourced.
|
||||||
|
#
|
||||||
|
# Contract:
|
||||||
|
# * user-facing output goes to stdout, diagnostics/warnings to stderr,
|
||||||
|
# * no helper calls exit except tui_die (and it only does so when called),
|
||||||
|
# * no helper ever prints a secret, and nothing here writes to the log,
|
||||||
|
# * every helper tolerates `set -u`: optional arguments use ${x:-} defaults.
|
||||||
|
#
|
||||||
|
# shellcheck shell=bash
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Colors. Disabled automatically when stdout is not a terminal, when NO_COLOR
|
||||||
|
# is set, or when the caller asks for "never".
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
TUI_COLOR_MODE="${TUI_COLOR_MODE:-auto}"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2034 # color variables are consumed by bf-menu
|
||||||
|
tui_init_colors() {
|
||||||
|
local want="${1:-$TUI_COLOR_MODE}"
|
||||||
|
local enable=0
|
||||||
|
case "$want" in
|
||||||
|
always) enable=1 ;;
|
||||||
|
never) enable=0 ;;
|
||||||
|
*)
|
||||||
|
if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
|
||||||
|
enable=1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [[ "$enable" -eq 1 ]]; then
|
||||||
|
C_RESET=$'\033[0m'
|
||||||
|
C_BOLD=$'\033[1m'
|
||||||
|
C_DIM=$'\033[2m'
|
||||||
|
C_RED=$'\033[31m'
|
||||||
|
C_GREEN=$'\033[32m'
|
||||||
|
C_YELLOW=$'\033[33m'
|
||||||
|
C_BLUE=$'\033[34m'
|
||||||
|
C_MAGENTA=$'\033[35m'
|
||||||
|
C_CYAN=$'\033[36m'
|
||||||
|
else
|
||||||
|
C_RESET="" C_BOLD="" C_DIM="" C_RED="" C_GREEN="" C_YELLOW=""
|
||||||
|
C_BLUE="" C_MAGENTA="" C_CYAN=""
|
||||||
|
fi
|
||||||
|
export C_RESET C_BOLD C_DIM C_RED C_GREEN C_YELLOW C_BLUE C_MAGENTA C_CYAN
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Layout
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
TUI_WIDTH="${TUI_WIDTH:-78}"
|
||||||
|
|
||||||
|
tui_hr() {
|
||||||
|
local char="${1:--}"
|
||||||
|
local line
|
||||||
|
line="$(printf "%*s" "$TUI_WIDTH" "" | tr ' ' "$char")"
|
||||||
|
printf '%s%s%s\n' "$C_DIM" "$line" "$C_RESET"
|
||||||
|
}
|
||||||
|
|
||||||
|
# tui_banner TITLE [SUBTITLE]
|
||||||
|
tui_banner() {
|
||||||
|
local title="$1" subtitle="${2:-}"
|
||||||
|
printf '\n'
|
||||||
|
tui_hr '='
|
||||||
|
printf ' %s%s%s\n' "$C_BOLD$C_CYAN" "$title" "$C_RESET"
|
||||||
|
if [[ -n "$subtitle" ]]; then
|
||||||
|
printf ' %s%s%s\n' "$C_DIM" "$subtitle" "$C_RESET"
|
||||||
|
fi
|
||||||
|
tui_hr '='
|
||||||
|
printf '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
# tui_screen TITLE [SUBTITLE]
|
||||||
|
# Clears the terminal first when interactive so one screen fits one page; the
|
||||||
|
# clear is skipped for non-interactive runs (pipes, tests, logs).
|
||||||
|
tui_screen() {
|
||||||
|
if [[ -t 1 && "${TUI_CLEAR:-yes}" == "yes" ]]; then
|
||||||
|
printf '\033[H\033[2J'
|
||||||
|
fi
|
||||||
|
tui_banner "${1:-}" "${2:-}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# tui_item NUMBER LABEL [DESCRIPTION]
|
||||||
|
tui_item() {
|
||||||
|
local number="$1" label="$2" desc="${3:-}"
|
||||||
|
if [[ -n "$desc" ]]; then
|
||||||
|
printf ' %s%2s%s) %-46s %s%s%s\n' "$C_BOLD" "$number" "$C_RESET" "$label" "$C_DIM" "$desc" "$C_RESET"
|
||||||
|
else
|
||||||
|
printf ' %s%2s%s) %s\n' "$C_BOLD" "$number" "$C_RESET" "$label"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# tui_section LABEL -- visual separator inside a long menu.
|
||||||
|
tui_section() {
|
||||||
|
printf '\n %s%s%s\n' "$C_MAGENTA" "${1:-}" "$C_RESET"
|
||||||
|
}
|
||||||
|
|
||||||
|
# tui_footer [BACK_LABEL] -- every screen ends with back and quit.
|
||||||
|
tui_footer() {
|
||||||
|
local back="${1:-Back}"
|
||||||
|
printf '\n'
|
||||||
|
tui_hr '-'
|
||||||
|
printf ' %s%2s%s) %-46s %s%2s%s) %s\n' \
|
||||||
|
"$C_BOLD" "0" "$C_RESET" "$back" "$C_BOLD" "q" "$C_RESET" "Quit the console"
|
||||||
|
tui_hr '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Input
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# tui_read PROMPT -> TUI_REPLY
|
||||||
|
# Returns 0 on a line, 1 on EOF (stdin closed / non-interactive); on EOF the
|
||||||
|
# reply is "q" so the caller's menu loop quits instead of spinning forever.
|
||||||
|
tui_read() {
|
||||||
|
local prompt="${1:-} "
|
||||||
|
TUI_REPLY=""
|
||||||
|
printf '%s' "$prompt"
|
||||||
|
if ! IFS= read -r TUI_REPLY; then
|
||||||
|
TUI_REPLY="q"
|
||||||
|
printf '\n'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
# Trim surrounding whitespace so " 2 " is accepted like "2".
|
||||||
|
TUI_REPLY="${TUI_REPLY#"${TUI_REPLY%%[![:space:]]*}"}"
|
||||||
|
TUI_REPLY="${TUI_REPLY%"${TUI_REPLY##*[![:space:]]}"}"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# tui_prompt LABEL [DEFAULT] -> TUI_REPLY
|
||||||
|
tui_prompt() {
|
||||||
|
local label="$1" default="${2:-}" suffix=""
|
||||||
|
if [[ -n "$default" ]]; then
|
||||||
|
suffix=" [$default]"
|
||||||
|
fi
|
||||||
|
tui_read "${label}${suffix}: " || true
|
||||||
|
if [[ -z "$TUI_REPLY" ]]; then
|
||||||
|
TUI_REPLY="$default"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# tui_confirm QUESTION [yes|no] -- default is NO: only an explicit y/yes proceeds.
|
||||||
|
tui_confirm() {
|
||||||
|
local question="$1" default="${2:-no}" suffix="[y/N]"
|
||||||
|
[[ "$default" == "yes" ]] && suffix="[Y/n]"
|
||||||
|
tui_read "${question} ${suffix} " || true
|
||||||
|
case "${TUI_REPLY,,}" in
|
||||||
|
y|yes) return 0 ;;
|
||||||
|
n|no) return 1 ;;
|
||||||
|
"")
|
||||||
|
[[ "$default" == "yes" ]] && return 0
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# tui_confirm_token QUESTION TOKEN -- the operator must type the exact token.
|
||||||
|
# Used for the highest-risk operations (production waves, fleet-wide runs).
|
||||||
|
tui_confirm_token() {
|
||||||
|
local question="$1" token="$2"
|
||||||
|
tui_warn "$question"
|
||||||
|
tui_read "$(printf 'Type %s%s%s to confirm (anything else cancels): ' "$C_BOLD" "$token" "$C_RESET")" || true
|
||||||
|
[[ "$TUI_REPLY" == "$token" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# tui_ask_run QUESTION -> 0 = run, 2 = run with --check, 1 = cancel
|
||||||
|
tui_ask_run() {
|
||||||
|
local question="$1"
|
||||||
|
tui_read "${question} [y=run / c=run with --check / N=cancel] " || true
|
||||||
|
case "${TUI_REPLY,,}" in
|
||||||
|
y|yes) return 0 ;;
|
||||||
|
c|check) return 2 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# tui_menu_index NUMBER MAX -- true when NUMBER selects an item of the menu.
|
||||||
|
tui_menu_index() {
|
||||||
|
local number="$1" max="$2"
|
||||||
|
[[ "$number" =~ ^[0-9]+$ ]] || return 1
|
||||||
|
((number >= 1 && number <= max)) || return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
tui_pause() {
|
||||||
|
local message="${1:-Press Enter to continue}"
|
||||||
|
if [[ -t 0 ]]; then
|
||||||
|
tui_read "${C_DIM}${message}${C_RESET}" || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Messages
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
tui_msg() { printf '%s\n' "$*"; }
|
||||||
|
tui_info() { printf '%s[i]%s %s\n' "$C_CYAN" "$C_RESET" "$*"; }
|
||||||
|
tui_ok() { printf '%s[OK]%s %s\n' "$C_GREEN" "$C_RESET" "$*"; }
|
||||||
|
tui_warn() { printf '%s[WARN]%s %s\n' "$C_YELLOW" "$C_RESET" "$*" >&2; }
|
||||||
|
tui_error(){ printf '%s[ERROR]%s %s\n' "$C_RED" "$C_RESET" "$*" >&2; }
|
||||||
|
tui_note() { printf '%s%s%s\n' "$C_DIM" "$*" "$C_RESET"; }
|
||||||
|
|
||||||
|
# tui_kv KEY VALUE -- aligned key/value line for information screens.
|
||||||
|
tui_kv() {
|
||||||
|
printf ' %-24s %s\n' "$1" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
# tui_die MESSAGE [EXIT_CODE] -- last resort: report and exit with a non-zero
|
||||||
|
# status. Everything user-recoverable should return a status instead.
|
||||||
|
tui_die() {
|
||||||
|
local message="$1" code="${2:-1}"
|
||||||
|
tui_error "$message"
|
||||||
|
exit "$code"
|
||||||
|
}
|
||||||
611
docs/33-OPERATOR-CONSOLE.md
Normal file
611
docs/33-OPERATOR-CONSOLE.md
Normal file
|
|
@ -0,0 +1,611 @@
|
||||||
|
# 33 — Operatör Konsolu Rehberi (`bf-menu`)
|
||||||
|
|
||||||
|
> Kısa özet: Bu rehber, Linux/DevOps'a yeni başlayan bir operatörün Blueforce cihazını **git deposundan** kurmasını ve tek giriş noktası olan `sudo bf-menu` **kategori menüsü** ile cihazı işletmesini adım adım anlatır. Kurulum, filo işlemleri, merkez hazırlığı, araçlar ve cihaz bilgisi ekranlarının tamamı bu menüden açılır; komut ezberlemek gerekmez.
|
||||||
|
|
||||||
|
- Dosya: `docs/33-OPERATOR-CONSOLE.md`
|
||||||
|
- İlgili kararlar: `24-DECISION-LOG.md#K-09` (golden image + kurucu), `#K-12` (kimlik formatı `^[0-9]{8}$`), `#K-13` (SSH anahtar zorunlu), `#K-11` (onaysız update yasağı), `#K-21` (durum modeli), `#K-22` (RDP her zaman hazır / `bf-gui-*` yalnız yerel GUI)
|
||||||
|
- Uygulama: `admin/bf-menu` (+ `admin/lib/tui.sh`, `admin/lib/targets.sh`), `admin/bf-bootstrap.sh`, `admin/bf-creds`
|
||||||
|
- Durum: [ ] Taslak — ekran çıktıları **örnektir (taklit)** ve LAB'da alınan gerçek çıktıyla güncellenir
|
||||||
|
|
||||||
|
> **Dürüstlük notu (bu reponun kuralı):** Aşağıdaki menü ekranları ve çıktılar, tasarımı anlatmak için yazılmış **örnek ekranlardır**; terminal genişliğine, renklerin kapalı olmasına ve envanter içeriğine göre birebir farklı görünebilir. Gerçek çıktılar ilk LAB koşusunda bu dosyaya işlenir (§10, §13).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Amaç
|
||||||
|
|
||||||
|
**Bu araç nedir?** `bf-menu`, cihaza `sudo bf-menu` yazınca açılan, numaralarla gezilen bir **operatör konsoludur**. Ekranda 5 kategori çıkar (Kurulum, Cihaz işlemleri, Merkez hazırlığı, Araçlar, Cihaz bilgisi); bir numara yazıp Enter'a basarsın, konsol senin adına doğru komutu çalıştırır ve ne yaptığını Türkçe/İngilizce satırlarla gösterir.
|
||||||
|
|
||||||
|
**Neyi kolaylaştırır?** Normalde cihazı kurmak, durumunu okumak, teşhis paketi almak veya bir filo işlemi başlatmak için onlarca komutu ve doğru sırasını bilmek gerekir. `bf-menu` bunları numaralı başlıklar altında toplar, **çalıştırmadan önce komutu ekranda gösterip onay ister**, tehlikeli işlemlerde iki kez sorar ve her işlemi bir denetim log'una (`/var/log/blueforce-console.log`, mod `600`) yazar. Kısacası: ezberleyeceğin tek şey `sudo bf-menu` komutudur.
|
||||||
|
|
||||||
|
## 2. Kapsam
|
||||||
|
|
||||||
|
- Kapsam içi: Ubuntu'da ilk kurulum (git'ten indir + bootstrap), ana menü ve 5 kategori, tek cihaz/grup/tüm filo hedef seçimi, uçtan uca örnek seans, şifre dosyası (`bf-creds`) davranışı, en sık 5 hata ve güvenlik kuralları.
|
||||||
|
- Kapsam dışı: ISO/firstboot/offline provisioning yolu (26, 27 — artık **ikincil/opsiyonel**), merkez sunucularının kurulumu (07, 09 — konsol yalnız **hazırlar ve denetler**, kurmaz), update onay zincirinin merkez tarafı (10, 21), rol bazlı eğitim müfredatı (31).
|
||||||
|
|
||||||
|
## 3. Kararlar
|
||||||
|
|
||||||
|
```text
|
||||||
|
KARAR: Cihaz ve filo işlemlerinin tek operatör giriş noktası `sudo bf-menu` kategori menüsüdür; operatör komut ezberlemez, numaralı kategoriden seçer.
|
||||||
|
GEREKÇE: Operatör profili Linux/DevOps'a yeni başlayan kişidir; serbest komut yazımı 700 cihazda yazım hatasını filo arızasına çevirir (K-11 blast-radius). Konsol her komutu çalıştırmadan önce gösterir ve onay ister; dalga kapılı playbook'lara bayrak sessizce geçirilmez, operatör kapıyı ayrıca onaylar.
|
||||||
|
ALTERNATİF: Operatörün dokümandan komut kopyalaması — sıra atlama ve yanlış hedef seçme riski yüksek; elendi. Yalnız merkez Semaphore UI — saha cihazında çevrimdışıyken erişilemez; merkez tarafı için korunur (09).
|
||||||
|
RİSK: Konsolun tablosu, altındaki playbook'tan ayrışır ve yanlış kapıyla çalıştırır; azaltma: konsol her playbook'un kapsamını (simple/wave/release) playbook'un kendisinden okuyup karşılaştırır, uyuşmazlıkta **çalıştırmayı reddeder** (`admin/bf-menu` → `bf_playbook_check`).
|
||||||
|
MALİYET: Ücretsiz.
|
||||||
|
LİSANS: Yok (kurum içi operatör aracı).
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
KARAR: Kurulum git deposundan yapılır: depo klonlanır ve `sudo admin/bf-bootstrap.sh` (veya `curl -fsSL <repo>/admin/bf-bootstrap.sh | sudo bash`) çalıştırılır. ISO/firstboot yolu (26, 27) silinmez, ikincil/opsiyonel yol olarak korunur.
|
||||||
|
GEREKÇE: Dağıtım imajı/ISO üretimi bu fazda yapılmayacaktır; en hızlı doğrulanabilir yol, sürümü Git geçmişiyle izlenebilen tek bir bootstrap scriptidir. Bootstrap repoyu `/opt/blueforce-linux` (veya bulunduğu checkout) altına alır, ön koşulları (git, curl, ansible-core) kurar ve `bf-*` araçlarını `/usr/local/bin`'e bağlar; cihaz kimliği (bayi no) hiçbir zaman bir imaja gömülmez (K-09/K-12).
|
||||||
|
ALTERNATİF: Tek USB Field OS ISO (K-20) — üretim/doğrulama maliyeti yüksek olduğu için bu fazda birincil yol değil; yedek olarak korunur. USB'ye elle kopyalama — sürüm izi ve güncellenebilirlik yok; elendi.
|
||||||
|
RİSK: `curl | bash` deseni kötüye kullanıma açıktır; azaltma: script yalnız resmi depo adresinden çalıştırılır, `git clone` ile içeriği önce incelenebilir, secret yazmaz, `git reset`/`stash` yapmaz ve yerel değişiklikleri asla silmez.
|
||||||
|
MALİYET: Ücretsiz.
|
||||||
|
LİSANS: Yok (kurum içi script).
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
KARAR: Şifre/kimlik bilgisi üretimi OPSİYONELDİR ve VARSAYILAN OLARAK KAPALIDIR; yalnız `bf-creds --generate` açıkça istendiğinde üretilir ve parola hiçbir zaman ekrana/loga/komut satırına yazılmaz.
|
||||||
|
GEREKÇE: SSH zaten anahtar zorunludur (parola girişi kapalı, `blueforce` hesabı kilitli — K-13); kullanılmayan bir parola üretmek sızma yüzeyini büyütür. Varsayılan kapalı olması secret'ın git'e/medyaya/support bundle'a kaçmasını da baştan engeller.
|
||||||
|
ALTERNATİF: Kurulumda otomatik parola üretimi — hiçbir kanal bu parolayı kullanmadığı için gereksiz secret üretir; elendi. Parolayı repoya/ISO'ya gömmek — secret yasağına aykırı; yasak.
|
||||||
|
RİSK: Kayıt dosyası yanlış yere kopyalanıp paylaşılır; azaltma: dosya `/root` altında mod `600`, `--fetch` hedefi `admin/secrets/` git-ignore'lu (dizin `700`), parola yalnız dosyaya yazılır.
|
||||||
|
MALİYET: Ücretsiz.
|
||||||
|
LİSANS: Yok.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Neden Bu Karar?
|
||||||
|
|
||||||
|
700 cihazlık bir filoda en pahalı hata, teknisyenin yanlış komutu yanlış cihazda çalıştırmasıdır. Konsol bu riski üç yerden küçültür: (1) operatör komut yazmaz, doğru işlemi seçer; (2) her komut **çalıştırılmadan önce ekranda** görünür ve onay ister — "ne çalışacak?" sorusu asla cevapsız kalmaz; (3) filo işlemleri hedefi (`--limit`) açıkça seçtirir ve dalga kapılarını (production için `production`, release kapısı için `GATE` yazma) operatöre ayrıca onaylatır. Git tabanlı kurulum ise "hangi sürüm sahada?" sorusunu `git log` ile yanıtlanabilir kılar. Şifre üretiminin varsayılan kapalı olması aynı felsefenin devamıdır: anahtar zaten var, ikinci bir gizli bilgi üretmenin faydası yok.
|
||||||
|
|
||||||
|
## 5. Alternatifler
|
||||||
|
|
||||||
|
| Alternatif | Artı | Eksi | Sonuç |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Numaralı kategori konsolu (`bf-menu`) | Keşfedilebilir, komutu gösterip onay ister, çevrimdışı çalışır, denetim log'u tutar | Konsol tablosu playbook'la senkron tutulmalı | **Seçildi** |
|
||||||
|
| Dokümandan komut kopyalama | Araç gerektirmez | Sıra atlama + yanlış hedef riski | Elendi |
|
||||||
|
| Yalnız Semaphore UI (web) | Güzel arayüz, merkezden yönetim | Saha cihazında çevrimdışı erişilemez; cihaz-kurulumunu kapsamaz | Merkez için korunur (09) |
|
||||||
|
| Grafik masaüstü aracı (GUI uygulaması) | Görsel | Cihazlar terminale boot eder (K-03), ek bağımlılık ister | Elendi |
|
||||||
|
| Tek düz komut scripti (`bf-do`) | Basit | Keşfedilebilirlik ve hedef seçimi yok, acemi için anlaşılmaz | Elendi |
|
||||||
|
|
||||||
|
## 6. Avantajlar
|
||||||
|
|
||||||
|
- Öğrenme maliyeti tek satır: `sudo bf-menu`.
|
||||||
|
- Her komut çalıştırılmadan önce gösterilir; tehlikeli işlemler iki kez onaylanır — yanlış komut/yanlış sıra riski ortadan kalkar.
|
||||||
|
- Filo işlemleri yalnız mevcut playbook'larla ve açık `--limit` ile çalışır; konsol **yeni playbook üretmez** ve envanter dışı bir cihaza dokunmaz.
|
||||||
|
- Çevrimdışı çalışır — ISO/firstboot yoluna bağımlı değildir.
|
||||||
|
- Denetim log'u (`/var/log/blueforce-console.log`, mod `600`) komutu, hedefi ve çıkış kodunu kaydeder; **secret ve ham playbook çıktısı log'a yazılmaz**.
|
||||||
|
|
||||||
|
## 7. Dezavantajlar
|
||||||
|
|
||||||
|
- Konsol tablosu ile playbook'lar arasında kapsam (scope) uyuşmazlığı olursa konsol çalıştırmayı reddeder; bu, playbook güncellemesiyle konsolun birlikte güncellenmesini zorunlu kılar (`admin/bf-menu` → `bf_playbook_check`).
|
||||||
|
- Kategoriler 2, 3 ve 5-filo için makinede `ansible-playbook` gerekir; yoksa konsol açıklama verir ama işlemi çalıştırmaz.
|
||||||
|
- Çok özel/tek seferlik işler konsol kataloğunda yoksa operatör kabuğa döner (kataloğa ekleme PR ile yapılır).
|
||||||
|
- TUI çıktısı log'a birebir kopyalanmaz; her seçim ayrı bir denetim satırı üretir (K-14).
|
||||||
|
|
||||||
|
## 8. Riskler
|
||||||
|
|
||||||
|
| Risk | Olasılık | Etki | Azaltma |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Konsol bayatlar, yanlış kapıyla çalıştırır | Orta | Yüksek | `bf_playbook_check` kapsam karşılaştırması uyuşmazlıkta reddeder; §10 testi her sürümde koşar |
|
||||||
|
| Operatör "tüm filo" hedefini yanlışlıkla seçer | Orta | Yüksek | Wave-scoped playbook'larda filo dalga dalga ilerler, her dalga **ayrı onaylanır**; production için `production` yazma zorunlu (09/10) |
|
||||||
|
| `curl | bash` ile sahte script çalıştırma | Düşük | Yüksek | Yalnız resmi depo adresi; `git clone` + gözden geçirme önerilir |
|
||||||
|
| Şifre kaydı yanlışlıkla paylaşılır | Düşük | Yüksek | Varsayılan kapalı; `/root/blueforce-credentials.txt` mod `600`; `admin/secrets/` git-ignore'lu (§9.5) |
|
||||||
|
| İşlem yarıda kalkar (elektrik) | Orta | Orta | Kurulum idempotent; menü "Resume after a failed run" sunar (§9.6) |
|
||||||
|
| Konsol, envanter dışı cihaza dokunur | Düşük | Yüksek | Hedefler envanterden çözülür; cihaz/grup bulunamazsa işlem reddedilir |
|
||||||
|
|
||||||
|
## 9. Uygulama Planı
|
||||||
|
|
||||||
|
Bu bölüm sıfırdan başlayan içindir. Her adımda **hangi komutu nereye yazacağın** ve **ekranda ne göreceğin** yazılıdır.
|
||||||
|
|
||||||
|
### 9.1 Ubuntu'da ilk kurulum — adım adım
|
||||||
|
|
||||||
|
**Ön koşullar (elinde olması gerekenler):**
|
||||||
|
|
||||||
|
- Ubuntu Server tabanlı bir makine (desteklenen baseline `24.04` / `26.04`).
|
||||||
|
- `sudo` yetkisi olan bir kullanıcı.
|
||||||
|
- Kurulum sırasında **internet** (repoyu git'ten indirmek için). Cihaz kurulduktan sonra çalışması için internet gerekmez.
|
||||||
|
|
||||||
|
**Adım 1 — Terminali aç.** Ekranda şöyle bir satır görürsün:
|
||||||
|
|
||||||
|
```text
|
||||||
|
emirhan@makine:~$
|
||||||
|
```
|
||||||
|
|
||||||
|
`$` işareti "senin yazacağın yer" demektir; **`$` işaretini sen yazmazsın.** Aşağıdaki komutları bu satıra yazıp Enter'a bas.
|
||||||
|
|
||||||
|
**Adım 2 — Git kurulu mu?**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Beklenen çıktı: `git version 2.x.y`. `command not found` yazarsa kur:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update && sudo apt install -y git
|
||||||
|
```
|
||||||
|
|
||||||
|
`[sudo] password for emirhan:` yazınca **klavyeden parolanı yaz** — ekranda hiçbir karakter görünmez, bu normaldir — sonra Enter.
|
||||||
|
|
||||||
|
**Adım 3 — Depoyu indir (klonla).**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/emirhanmamak-blueforce/Blueforce-Linux.git
|
||||||
|
cd Blueforce-Linux
|
||||||
|
```
|
||||||
|
|
||||||
|
Ekranda `Cloning into 'Blueforce-Linux'...` görürsün ve komut `$` satırına döner.
|
||||||
|
|
||||||
|
**Adım 4 — Bootstrap'ı çalıştır.** İki yol var, ikisi de aynı sonucu verir.
|
||||||
|
|
||||||
|
*Yol A — depo içinden (önerilen; önce içeriği inceleyebilirsin):*
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo admin/bf-bootstrap.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
*Yol B — tek satırla (depoyu klonlamadan; repoyu `/opt/blueforce-linux` altına klonlar):*
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/emirhanmamak-blueforce/Blueforce-Linux/main/admin/bf-bootstrap.sh | sudo bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Bootstrap ne yapar (kendi özet çıktısı, örnek):
|
||||||
|
|
||||||
|
```text
|
||||||
|
bf-bootstrap: ubuntu 26.04 is a supported baseline
|
||||||
|
bf-bootstrap: dependency check
|
||||||
|
bf-bootstrap: dependencies present: git curl ansible-playbook
|
||||||
|
bf-bootstrap: cloning https://github.com/emirhanmamak-blueforce/Blueforce-Linux.git (branch main) into /opt/blueforce-linux
|
||||||
|
bf-bootstrap: linked 26 tools into /usr/local/bin
|
||||||
|
bf-bootstrap: bf-menu ready: /usr/local/bin/bf-menu
|
||||||
|
|
||||||
|
bf-bootstrap: summary
|
||||||
|
repository : /opt/blueforce-linux
|
||||||
|
branch : main
|
||||||
|
revision : <git hash>
|
||||||
|
tools : 26 linked into /usr/local/bin
|
||||||
|
bf-menu : /usr/local/bin/bf-menu
|
||||||
|
next : sudo bf-menu
|
||||||
|
|
||||||
|
Installation complete. To get started: sudo bf-menu
|
||||||
|
Kurulum tamam. Başlamak için: sudo bf-menu
|
||||||
|
```
|
||||||
|
|
||||||
|
| Bootstrap ne yapar | Ne yapmaz |
|
||||||
|
|---|---|
|
||||||
|
| Ön koşulları kurar (`git`, `curl`, `ansible-core`) | Cihaz kimliği (bayi no) atamaz |
|
||||||
|
| Repoyu klonlar veya mevcut checkout'u **fast-forward** eder | `git reset` / `git stash` yapmaz; yerel değişikliği **silmez** |
|
||||||
|
| `admin/bf-*`, `scripts/diagnostics/bf-*`, `scripts/maintenance/bf-*` araçlarını `/usr/local/bin`'e bağlar | Parola/secret üretmez veya indirmez |
|
||||||
|
| `admin/bf-menu`'yü çalıştırılabilir yapıp `sudo bf-menu`'yü hazırlar | Disk bölmez, servis silmez, onaysız paket güncellemez |
|
||||||
|
|
||||||
|
> `sudo admin/bf-bootstrap.sh --check` **hiçbir şeyi değiştirmeden** ne yapılacağını raporlar (root bile gerekmez). Önce bunu çalıştırmak iyi alışkanlıktır.
|
||||||
|
|
||||||
|
**Adım 5 — Menüyü aç ve doğrula.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bf-menu
|
||||||
|
```
|
||||||
|
|
||||||
|
Menü açılıyorsa kurulum tamam. Bundan sonra her iş için aynı komut:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bf-menu
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Bundan sonra ezberleyeceğin tek şey bu komuttur.** Menüden çıkmak için ana ekranda `q`; alt menülerde önce `0` (geri), sonra `q` (çıkış).
|
||||||
|
|
||||||
|
### 9.2 Menüyü kullanma — kategori kategori
|
||||||
|
|
||||||
|
`sudo bf-menu` çalıştırınca **ana menü** açılır. Bir numara yazıp Enter'a bas.
|
||||||
|
|
||||||
|
**Ana menü (örnek ekran):**
|
||||||
|
|
||||||
|
```text
|
||||||
|
==============================================================================
|
||||||
|
Blueforce operator console 1.0.0
|
||||||
|
repo: /opt/blueforce-linux
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
Categories
|
||||||
|
|
||||||
|
1) Installation this device, local one-click installer
|
||||||
|
2) Device operations fleet playbooks, explicit target
|
||||||
|
3) Central (preparation) verify and prepare central integration
|
||||||
|
4) Tools repository and local helpers
|
||||||
|
5) Device information read-only information screens
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
q) Quit the console
|
||||||
|
Log: /var/log/blueforce-console.log
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
Choice >
|
||||||
|
```
|
||||||
|
|
||||||
|
Kategori kategori ne yapar ve hangi soruları sorar:
|
||||||
|
|
||||||
|
**1) Installation (bu cihaz)** — yalnız **senin oturduğun makineyi** kurar; hiçbir filo cihazına dokunmaz.
|
||||||
|
|
||||||
|
```text
|
||||||
|
1) Pre-flight check installer --check, writes nothing
|
||||||
|
2) Install this device full run, asks dealer number
|
||||||
|
3) Resume after a failed run installer --resume
|
||||||
|
4) Run a single module installer --only
|
||||||
|
5) Show module state /var/lib/blueforce/install-state
|
||||||
|
6) Show the installer log tail of /var/log/blueforce-install.log
|
||||||
|
```
|
||||||
|
Sorduğu sorular: **`Dealer number (8 digits) >`**; 2. ve 3. seçenekte ayrıca "offline repo'dan mı kurulsun?" ve "zaten OK olan modüller atlansın mı?" onayları; son olarak **`Run this command? [y=run / c=run with --check / N=cancel]`**. 2, 3 ve 4 numaralı seçenekler `root` ister (konsolu `sudo` ile açtıysan hazırdır). Format yanlışsa (`^[0-9]{8}$` uymuyorsa) konsol kurulumu başlatmaz.
|
||||||
|
|
||||||
|
**2) Device operations (filo)** — yalnız repodaki **mevcut** playbook'ları, açık bir hedefle çalıştırır.
|
||||||
|
|
||||||
|
```text
|
||||||
|
1) Read-only reports 11 operations, no change on any device
|
||||||
|
2) Maintenance actions 10 operations, some destructive
|
||||||
|
3) Show inventory targets groups, hosts and counts
|
||||||
|
```
|
||||||
|
|
||||||
|
*2.1 Read-only fleet reports (11 işlem, hiçbiri cihazı değiştirmez):*
|
||||||
|
|
||||||
|
| # | İşlem | Ne yapar |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Ping / reachability | SSH erişilebilirliği (salt-okunur) |
|
||||||
|
| 2 | Uptime | Çalışma süresi ve yük |
|
||||||
|
| 3 | Disk usage | Disk kullanımı; eşik üstünde uyarır (eşik sorulur) |
|
||||||
|
| 4 | Docker status | Docker servisi ve konteynerler |
|
||||||
|
| 5 | Package versions | Pinlenmiş paketlerin kurulu sürümleri |
|
||||||
|
| 6 | Pending security updates | **Yalnız rapor**; asla kurmaz |
|
||||||
|
| 7 | Provisioning phase report | Cihaz başına `state.json` fazı |
|
||||||
|
| 8 | Enrollment + central evidence | ENROLLED/READY iddialarını merkezi kanıta karşı denetler |
|
||||||
|
| 9 | Field OS version report | Sürüm; istenirse drift kontrolü (beklenen sürüm sorulur) |
|
||||||
|
| 10 | Remote access channels | SSH/xRDP/RustDesk/WireGuard hazırlığı |
|
||||||
|
| 11 | Offline readiness report | Offline provisioning yerel ön koşulları |
|
||||||
|
|
||||||
|
*2.2 Fleet maintenance (10 işlem, bir kısmı yıkıcı):*
|
||||||
|
|
||||||
|
| # | İşlem | Not |
|
||||||
|
|---|---|---|
|
||||||
|
| 1–3 | Restart RDP / RustDesk / WireGuard | Servis yeniden başlatma |
|
||||||
|
| 4 | Restart an allowlisted service | Yalnız izinli servis adları (`blueforce-agent`, `docker`, `ssh`, `sshd`, `systemd-journald`, `cron`) |
|
||||||
|
| 5 | GUI off (headless) | Dalgayı `multi-user.target`'e alır |
|
||||||
|
| 6 | GUI on (maintenance) | Grafik oturumu geri açar |
|
||||||
|
| 7 | Collect logs / support bundle | Zaman damgalı journal + snapshot paketi (satır sayısı sorulur) |
|
||||||
|
| 8 | Run an approved script | Yalnız `/opt/blueforce/bin` ve `/usr/local/sbin` |
|
||||||
|
| 9 | Controlled reboot | Seri yeniden başlatma + sağlık kapısı (bekleme süresi sorulur) |
|
||||||
|
| 10 | Deploy approved package update | `package=version` pinleri + sağlık kapısı |
|
||||||
|
|
||||||
|
> **`bf-gui-off` xRDP'i durdurmaz.** `xrdp`/`xrdp-sesman` her zaman enable+active kalır; `bf-gui-*` yalnız **yerel fiziksel** grafik katmanını kontrol eder (K-22). "GUI kapalı ≠ RDP kapalı."
|
||||||
|
|
||||||
|
**3) Central (preparation)** — merkez entegrasyonunu **denetler/hazırlar**; merkez sunucusunu **kurmaz**.
|
||||||
|
|
||||||
|
```text
|
||||||
|
1) Central readiness report inventory, placeholders, admin key
|
||||||
|
2) Show configured central endpoints WireGuard hub, monitoring, mirror
|
||||||
|
3) Probe central endpoints DNS + TCP, read-only
|
||||||
|
4) Wave rollout gate state per-wave device counts and gate rules
|
||||||
|
5) Central preparation checklist what must exist before enrollment
|
||||||
|
```
|
||||||
|
|
||||||
|
**4) Tools** — repo ve yerel yardımcılar.
|
||||||
|
|
||||||
|
```text
|
||||||
|
1) Update this checkout git pull --ff-only
|
||||||
|
2) Environment and reference info paths, versions, resolved settings
|
||||||
|
3) Show the operation catalogue --list output
|
||||||
|
4) Local offline gate bf-check-local
|
||||||
|
5) Enrollment gate bf-check-enrollment
|
||||||
|
6) Ready gate bf-check-ready
|
||||||
|
7) Deep diagnostics bf-diagnostics (or bf-status)
|
||||||
|
8) Field OS release identity bf-release
|
||||||
|
9) Hardware inventory bf-hardware-inventory.sh
|
||||||
|
10) Support bundle bf-support-bundle --out-dir
|
||||||
|
11) Show the console log last 40 lines of the audit log
|
||||||
|
```
|
||||||
|
|
||||||
|
**5) Device information** — salt-okunur bilgi ekranları.
|
||||||
|
|
||||||
|
```text
|
||||||
|
1) Identity hostname, device id, dealer number
|
||||||
|
2) Health summary bf-status
|
||||||
|
3) Provisioning and enrollment state state.json whitelist + bf-enrollment-status
|
||||||
|
4) Network addresses, route, DNS, WireGuard peers
|
||||||
|
5) Disk, memory, CPU df, free, load
|
||||||
|
6) Installed pinned packages dpkg-query
|
||||||
|
7) Service status ssh, xrdp, rustdesk, docker, wg, meshagent
|
||||||
|
8) Recent journal errors journalctl -p err
|
||||||
|
9) Inventory summary waves, groups, counts
|
||||||
|
10) Resolve a target check a dealer number or group
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Not (WireGuard):** Konsol WireGuard private/preshared key'lerini **hiçbir zaman istemez, göstermez veya loglamaz**; yalnız peer durumunu okur.
|
||||||
|
|
||||||
|
### 9.3 Cihaz seçimi: tek cihaz mı grup mu
|
||||||
|
|
||||||
|
Kategori **2**'de bir işlem seçtiğinde konsol önce **hedefi** sorar:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Target selection (mode: wave)
|
||||||
|
1) Single device by dealer number 8 digits, e.g. 12010101
|
||||||
|
2) Group / wave lab, pilot_1, pilot_2, wave_1, wave_2, production
|
||||||
|
3) All devices in the fleet asks per wave for wave-scoped playbooks
|
||||||
|
9) Type the target directly dealer number, group or 'all'
|
||||||
|
Target >
|
||||||
|
```
|
||||||
|
|
||||||
|
| Hedef türü | Menüde ne yazarsın | Örnek | Boyut | Tipik kullanım |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Tek cihaz | bayi no (8 hane) | `12010101` | 1 | Arıza teşhisi, tek cihaz bakımı |
|
||||||
|
| Grup / dalga | grup adı | `lab` | 2 | Yeni playbook denemesi (önce LAB kuralı) |
|
||||||
|
| Grup / dalga | grup adı | `pilot_1` / `pilot_2` / `wave_1` / `wave_2` | 5 / 20 / 50 / 100 | Kademeli yayılım (K-11) |
|
||||||
|
| Tüm filo | `3` (veya doğrudan `all`) | — | kalan | Yalnız onaylı; dalga dalga ilerler |
|
||||||
|
|
||||||
|
Kurallar (konsolun kendi davranışı):
|
||||||
|
|
||||||
|
- **Tek cihaz** normalde en güvenli seçenektir; cihaz envanterde yoksa konsol **reddeder** (`is not in the inventory`).
|
||||||
|
- **Grup / dalga** seçiminde liste, her grubun cihaz sayısıyla ve onaylı dalga olup olmadığıyla gösterilir.
|
||||||
|
- **Wave-scoped** bir playbook'ta tüm filo seçilirse konsol, dalgaları **sırayla** (`lab → pilot_1 → pilot_2 → wave_1 → wave_2 → production`) işler ve **her dalga için ayrı ayrı** onay ister; bir dalga başarısız olursa sonraki dalgaya **geçmez**.
|
||||||
|
- **Release-gated** playbook'lar (reboot, deploy-update) tek cihaz veya tüm filo kabul etmez; **tek onaylı dalga** ister.
|
||||||
|
- **production** dalgası ayrıca `production` yazdırılarak, release kapısı ise `GATE` yazdırılarak onaylanır. Hiçbir kapı sessizce geçilmez.
|
||||||
|
|
||||||
|
### 9.4 Uçtan uca örnek seans (örnek ekran çıktısı)
|
||||||
|
|
||||||
|
Aşağıdaki iki kısa seans, kurulumdan günlük işletime tipik akışı gösterir. (Çıktılar **örnektir**.)
|
||||||
|
|
||||||
|
**Seans A — Yeni cihazın kurulumu (bayi no girme)**
|
||||||
|
|
||||||
|
```text
|
||||||
|
emirhan@makine:~$ sudo bf-menu
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
Blueforce operator console 1.0.0
|
||||||
|
repo: /opt/blueforce-linux
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
Categories
|
||||||
|
1) Installation 2) Device operations 3) Central (preparation)
|
||||||
|
4) Tools 5) Device information
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
q) Quit the console
|
||||||
|
Log: /var/log/blueforce-console.log
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
Choice > 1
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
1) Installation (this device)
|
||||||
|
Local install only -- no fleet device is touched here
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
1) Pre-flight check installer --check, writes nothing
|
||||||
|
2) Install this device full run, asks dealer number
|
||||||
|
3) Resume after a failed run installer --resume
|
||||||
|
4) Run a single module installer --only
|
||||||
|
5) Show module state /var/lib/blueforce/install-state
|
||||||
|
6) Show the installer log tail of /var/log/blueforce-install.log
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
0) Back q) Quit the console
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
Choice > 1
|
||||||
|
Read-only pre-flight: the installer creates no log, state or directory in --check mode.
|
||||||
|
Dealer number (8 digits) > 12010193
|
||||||
|
|
||||||
|
Pre-flight check for BF-12010193
|
||||||
|
|
||||||
|
bash /opt/blueforce-linux/scripts/install/blueforce-install.sh --dealer-id 12010193 --check
|
||||||
|
|
||||||
|
[1/18] precheck ........ OK [2/18] system ........ OK
|
||||||
|
... [18/18] final-check ... OK
|
||||||
|
|
||||||
|
Press Enter to continue
|
||||||
|
```
|
||||||
|
|
||||||
|
Şimdi gerçek kurulum (aynı menüden **2**):
|
||||||
|
|
||||||
|
```text
|
||||||
|
Choice > 2
|
||||||
|
Dealer number (8 digits) > 12010193
|
||||||
|
Install from the local offline repository (--offline)? [y/N] n
|
||||||
|
|
||||||
|
[i] Target device: BF-12010193 (bf-12010193)
|
||||||
|
|
||||||
|
Command
|
||||||
|
bash /opt/blueforce-linux/scripts/install/blueforce-install.sh --dealer-id 12010193
|
||||||
|
|
||||||
|
[WARN] This is a DESTRUCTIVE operation: install on BF-12010193
|
||||||
|
Proceed? [y=run / c=run with --check / N=cancel] y
|
||||||
|
|
||||||
|
[1/18] precheck ..................... OK
|
||||||
|
[2/18] system ....................... OK
|
||||||
|
...
|
||||||
|
[18/18] final-check .................. OK
|
||||||
|
|
||||||
|
[OK] Command finished successfully (exit 0).
|
||||||
|
Press Enter to continue
|
||||||
|
```
|
||||||
|
|
||||||
|
Kurulum sonunda cihaz `PROVISIONED_OFFLINE` olur; **`READY` değildir** — merkeze kayıt (enrollment) ve merkezi kanal kanıtı gerekir (K-21). Bunu görmek için kategori **5 → 3** (Provisioning and enrollment state) açılır.
|
||||||
|
|
||||||
|
**Seans B — Günlük kontrol (salt-okunur rapor) ve teşhis paketi**
|
||||||
|
|
||||||
|
```text
|
||||||
|
Choice > 2
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
2) Device operations
|
||||||
|
Existing playbooks only, always with an explicit target
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
1) Read-only reports 11 operations, no change on any device
|
||||||
|
2) Maintenance actions 10 operations, some destructive
|
||||||
|
3) Show inventory targets groups, hosts and counts
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
0) Back q) Quit the console
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
Choice > 1
|
||||||
|
|
||||||
|
... 1) Ping / reachability 2) Uptime 3) Disk usage 4) Docker status ...
|
||||||
|
|
||||||
|
Choice > 1
|
||||||
|
|
||||||
|
Target selection (mode: any)
|
||||||
|
1) Single device by dealer number 8 digits, e.g. 12010101
|
||||||
|
2) Group / wave lab, pilot_1, pilot_2, wave_1, wave_2, production
|
||||||
|
3) All devices in the fleet
|
||||||
|
9) Type the target directly dealer number, group or 'all'
|
||||||
|
Target > 1
|
||||||
|
Dealer number (8 digits) > 12010193
|
||||||
|
|
||||||
|
Command
|
||||||
|
ansible-playbook -i ansible/inventory/hosts.yml ansible/playbooks/bf-ping.yml --limit bf-12010193
|
||||||
|
|
||||||
|
Run this command? [y=run / c=run with --check / N=cancel] y
|
||||||
|
|
||||||
|
PLAY [Ping / reachability] ...
|
||||||
|
bf-12010193 | SUCCESS => {"changed": false, "ping": "pong"}
|
||||||
|
|
||||||
|
[OK] Command finished successfully (exit 0).
|
||||||
|
```
|
||||||
|
|
||||||
|
Teşhis paketi için kategori **4 → 10** (Support bundle); paket yolu ekranda yazılır ve paket **parola, private key veya token içermez** (K-14).
|
||||||
|
|
||||||
|
### 9.5 Kimlik bilgileri / şifre dosyası (opsiyonel — varsayılan KAPALI)
|
||||||
|
|
||||||
|
**Önce şunu bil:** SSH bu cihazda **anahtar zorunludur**; parola girişi kapalıdır ve `blueforce` bakım hesabı kilitlidir (K-13). Yani günlük yönetim için parolaya ihtiyaç yoktur. Bu yüzden şifre üretimi **varsayılan olarak kapalıdır** ve yalnız açıkça istenirse yapılır.
|
||||||
|
|
||||||
|
| Konu | Davranış (`admin/bf-creds`) |
|
||||||
|
|---|---|
|
||||||
|
| Varsayılan | **Kapalı.** `sudo bf-creds` hiçbir parola üretmez; yalnız cihazda **zaten var olan** gizlileri raporlar. |
|
||||||
|
| Üretme | `sudo bf-creds --generate` — cihaz kaydını yazar (ayrıca `BF_GENERATE_CREDENTIALS=1`) |
|
||||||
|
| Kayıt dosyası | `/root/blueforce-credentials.txt` (mod `0600`) — cihaz kimliği (`BF-<no>`), hostname, oluşturma zamanı, WireGuard **public** anahtarı + private anahtarın **yolu** (içeriği değil), varsa RustDesk ID ve yerel **yönetici parolası** |
|
||||||
|
| Çekme (merkeze/kasaya) | `sudo bf-creds --fetch` — `<repo>/admin/secrets/<bayi-no>.txt` (dizin `0700`, dosya `0600`, **git-ignore'lu**) |
|
||||||
|
| Parola ekranda görünür mü? | **Hayır.** Parola yalnızca dosyaya yazılır; stdout/stderr'e, log'a veya komut satırına asla konmaz. |
|
||||||
|
| Git'e / ISO'ya / destek paketine girer mi? | **Hayır.** Secret hiçbir zaman depoya, medyaya veya support bundle'a konmaz. |
|
||||||
|
| SSH'ı değiştirir mi? | **Hayır.** `bf-creds` `sshd_config`'i düzenlemez, hesap açmaz/kilit açmaz, parola girişi yolunu açmaz. |
|
||||||
|
|
||||||
|
**Neden varsayılan kapalı?** Kullanılmayan her secret fazladan bir sızma yüzeyidir. Anahtar tabanlı erişim varken ikinci bir parola üretmek, sadece "git'e kaçabilecek yeni bir dosya" yaratır. İstisna (ör. yerel konsolda geçici bir parola isteyen bir ekip) `--generate` ile karşılanır ve kayıt dosyasının varlığı kayda geçer. **Varsayılanı değiştirmek bir karar gerektirir; sessizce açılmaz.**
|
||||||
|
|
||||||
|
### 9.6 Bir şey ters giderse — en sık 5 hata
|
||||||
|
|
||||||
|
| # | Belirti (ekranda ne görürsün) | Neden | Çözüm |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | `bf-menu: command not found` | Bootstrap çalışmamış veya araçlar bağlanmamış | Bootstrap'ı tekrar koş: `sudo admin/bf-bootstrap.sh`; sonra `sudo /usr/local/bin/bf-menu` ile tam yoldan dene |
|
||||||
|
| 2 | `[ERROR] This operation needs root on THIS device.` | Kurulum seçenekleri root ister; konsolu `sudo`'suz açtın | Konsolu `sudo bf-menu` ile aç (kural: bu işlem için konsol `Re-run the console with sudo: sudo admin/bf-menu` der) |
|
||||||
|
| 3 | `[ERROR] Invalid dealer number '1201019': it must match ^[0-9]{8}$ (8 digits, no spaces).` | Bayi no 8 haneli ve yalnız rakam olmalı | Tam 8 hane, sadece rakam gir; baştaki sıfırı silme. Örnek: `12010193` |
|
||||||
|
| 4 | `[WARN] ansible-playbook not found ... fleet operations cannot run.` veya `[WARN] ... is not available` | Makinede Ansible yok ya da araç PATH'te değil | `sudo apt install ansible` (reponun kurulumu `ansible-core`'u zaten kurar) ve araçları bağla: `sudo admin/bf-bootstrap.sh` |
|
||||||
|
| 5 | `[ERROR] Playbook not found: ansible/playbooks/...` / `Refusing to run ... the console expects scope 'X' but the playbook declares 'Y'.` | Konsol ile repo checkout'u uyuşmuyor (eski checkout veya `--repo` yanlış) | Kategori **4 → 1** ile checkout'u güncelle (`git pull --ff-only`) veya doğru repoyu ver: `sudo bf-menu --repo /opt/blueforce-linux` |
|
||||||
|
|
||||||
|
Ek olarak sık görülen iki durum:
|
||||||
|
|
||||||
|
- **`git clone` internet/DNS hatası** (`Could not resolve host`): İnternet/DNS'i kontrol et; kurulum için internet yalnız bu adımda gerekir.
|
||||||
|
- **`[ERROR] ... is not in the inventory`**: Cihaz veya grup `ansible/inventory`'de yok; hedefi envanterle eşle (02) veya `bf-bootstrap` ile checkout'u güncelle.
|
||||||
|
|
||||||
|
### 9.7 Güvenlik kuralları
|
||||||
|
|
||||||
|
1. **Secret git'e, ISO'ya veya destek paketine girmez.** Parola, private key ve token repoda/medyada bulunmaz; `bf-support-bundle` çıktısı secret sızdırmaz (K-14).
|
||||||
|
2. **Onaysız update yoktur.** Güncelleme yalnız kategori 2 → "Deploy approved package update" ile, `package=version` pinleri + dalga kapısı + sağlık kapısı altında yapılır (K-11). Serbest `apt upgrade`, `latest` imaj veya otomatik güncelleyici mimaride yoktur (kategori 2'de "Pending security updates" **yalnız rapor**tur).
|
||||||
|
3. **Yıkıcı işlemler onaylıdır.** Her komut çalıştırılmadan önce ekranda gösterilir, onay istenir; yıkıcı işlemler ayrıca ikinci kez sorulur; production dalgası için `production` ve release kapısı için `GATE` yazılması zorunludur. Disk bölme/silme konsolda otomatikleştirilmemiştir.
|
||||||
|
4. **SSH yalnız anahtarla.** Parola girişi kapalı, `blueforce` hesabı kilitli; paylaşılan private key yasaktır (K-13).
|
||||||
|
5. **Tek kullanımlık enrollment token loglanmaz.** Token konsol dışında bir kez girilir; denetim log'u ham çıktıyı ve secret'ı yazmaz (K-17).
|
||||||
|
6. **Cihaz kimliği imajda taşınmaz.** Bayi no yalnız ilk kurulum seansında girilir (K-09/K-12).
|
||||||
|
|
||||||
|
## 10. Test Planı
|
||||||
|
|
||||||
|
| Test | Beklenen sonuç | Ortam |
|
||||||
|
|---|---|---|
|
||||||
|
| `sudo admin/bf-bootstrap.sh` iki kez koşulur | İkinci koşu hata üretmez (idempotent), araçlar yerinde | LAB(2) |
|
||||||
|
| `sudo bf-menu --list` / `--list-targets` | İşlem kataloğu ve envanter hedefleri hatasız listelenir | LAB(2) |
|
||||||
|
| `sudo bf-menu` açılır, `q` ile çıkılır | Konsol hiçbir değişiklik yapmaz | LAB(2) |
|
||||||
|
| Kategori 1 → 1 kuru koşu (`--check`) | Değişiklik yok, rapor üretir | LAB(2) |
|
||||||
|
| Kategori 1 → 2 kurulum, bayi no `12010193` | `PROVISIONED_OFFLINE`; hostname `bf-12010193` | LAB(2) |
|
||||||
|
| Geçersiz bayi no (7/9 hane, harf) | Konsol kurulumu başlatmaz | LAB(2) |
|
||||||
|
| Kategori 2 → 2 → 9 reboot, tek cihaz | Onay istenir; cihaz yeniden başlar; log satırı yazılır | LAB(2) |
|
||||||
|
| Kategori 2 wave-scoped playbook, hedef "all" | Dalga dalga; her dalga ayrı onaylanır; hata sonraki dalgayı durdurur | LAB(2) |
|
||||||
|
| production dalgası denemesi | `production` yazılmadan çalışmaz | LAB(2) |
|
||||||
|
| Kategori 4 → 11 konsol log'u | Mod `600`; secret ve ham playbook çıktısı yok | LAB(2) |
|
||||||
|
| `sudo bf-creds` (bayraksız) | Hiçbir parola üretilmez, açıkça söyler | LAB(2) |
|
||||||
|
| `sudo bf-creds --generate` | Kayıt dosyası `0600`; parola ekranda görünmez | LAB(2) |
|
||||||
|
|
||||||
|
## 11. Rollback
|
||||||
|
|
||||||
|
1. Kurulum yarıda kaldıysa: kategori **1 → 3 "Resume after a failed run"** (`--resume`); modüller idempotenttir.
|
||||||
|
2. Yanlış bayi no girildiyse: doğru no ile tam yeniden koşu; kimlik modülü hostname'i üzerine yazar (K-12).
|
||||||
|
3. Bootstrap yanlış kurulduysa: repoyu güncelleyip (`git pull --ff-only`) yeniden koş; bootstrap **hiçbir şeyi silmez, `git reset`/`stash` yapmaz**, yerel çalışmanı korur.
|
||||||
|
4. Filo işlemi başarısız dalga bıraktıysa: "Deploy approved package update" geri alma zinciri önceki onaylı release manifestine döner (10, 30).
|
||||||
|
5. Çözülmeyen cihaz: 17-RECOVERY seviye merdiveni (L8 = temiz yeniden kurulum) uygulanır.
|
||||||
|
|
||||||
|
## 12. Kontrol Listesi
|
||||||
|
|
||||||
|
- [x] `admin/bf-bootstrap.sh`, `admin/bf-menu` ve `admin/bf-creds` repoda mevcut; `sudo bf-menu` tek giriş noktası.
|
||||||
|
- [x] Kurulum git tabanlı (clone + bootstrap); ISO yolu ikincil/opsiyonel olarak korunuyor (26 silinmedi).
|
||||||
|
- [x] Her komut çalıştırılmadan önce gösterilip onaylanıyor; yıkıcı işlemler iki kez soruyor.
|
||||||
|
- [x] Bayi no kapısı `^[0-9]{8}$`; geçersiz girişte kurulum başlamıyor.
|
||||||
|
- [x] Filo işlemleri açık `--limit` ile; wave-scoped `all` dalga dalga ilerliyor; production için `production` yazma zorunlu.
|
||||||
|
- [x] Konsol, playbook kapsamıyla uyuşmazsa çalıştırmayı reddediyor.
|
||||||
|
- [x] Şifre üretimi varsayılan **kapalı**; yalnız `bf-creds --generate` ile üretiliyor; parola ekrana/loga gitmiyor.
|
||||||
|
- [x] Kategori 2'de onaysız güncelleme seçeneği **yok**; "Pending security updates" yalnız rapor.
|
||||||
|
- [x] Denetim log'u mod `600`; secret ve ham playbook çıktısı yazılmıyor.
|
||||||
|
- [ ] Örnek ekran çıktıları gerçek LAB çıktısıyla değiştirildi.
|
||||||
|
|
||||||
|
## 13. Açık Sorular
|
||||||
|
|
||||||
|
- [ ] Konsol işlem kataloğunun playbook kümesiyle tam eşleşmesi: `bf-service-restart`/`bf-run-script` gibi yeni playbook'lar kataloğa nasıl eklenecek (süreç/PR şablonu) (sahibi: 09 + konsol sahibi).
|
||||||
|
- [ ] Merkez hazırlığı ekranlarının (kategori 3) gerçek merkez endpoint'leri geldiğinde hangi kanıtı "hazır" sayacağı (sahibi: 29/21 yazarları).
|
||||||
|
- [ ] `bf-creds --fetch` hedefinin (`admin/secrets/`) kurumsal kasa (vault) akışına bağlanması (sahibi: 06-ssh + merkezi admin).
|
||||||
|
- [ ] Konsol işlem geçmişinin merkezi log toplamaya (Loki/ELK, Faz-sonrası) dahil edilip edilmeyeceği (sahibi: 15-LOGGING yazarı).
|
||||||
|
- [ ] Git kurulumu ile ISO/firstboot yolunun uzun vadeli rolü: ISO tamamen arşiv mi, yedek yol mu kalır (sahibi: yönetim + 26 yazarı).
|
||||||
|
- [ ] Kurumsal proxy/air-gapped sahalarda `git clone` yerine iç ayna (mirror) deposu kullanılacak mı; bootstrap `--repo`/`BF_REPO_URL` ile buna hazır mı (sahibi: merkezi admin).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ek: Menü haritası
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
START["sudo bf-menu"] --> MAIN{"Ana menü — kategori seç<br/>q = çıkış"}
|
||||||
|
MAIN --> C1["1) Installation<br/>bu cihazı kurar (filo cihazına dokunmaz)"]
|
||||||
|
MAIN --> C2["2) Device operations<br/>mevcut playbook'lar + açık hedef"]
|
||||||
|
MAIN --> C3["3) Central (preparation)<br/>merkez entegrasyonunu denetle/hazırla"]
|
||||||
|
MAIN --> C4["4) Tools<br/>repo + yerel yardımcılar"]
|
||||||
|
MAIN --> C5["5) Device information<br/>salt-okunur bilgi ekranları"]
|
||||||
|
C1 --> I1["1 Kuru koşu --check<br/>2 Kurulum (bayi no)<br/>3 Devam --resume<br/>4 Tek modül --only<br/>5 Modül durumu / 6 Log"]
|
||||||
|
C2 --> P1["2.1 Salt-okunur raporlar (11)"]
|
||||||
|
C2 --> P2["2.2 Bakım işlemleri (10, yıkıcı onaylı)"]
|
||||||
|
C2 --> P3["2.3 Envanter hedefleri"]
|
||||||
|
C3 --> K1["Hazırlık raporu / endpoint'ler / probe<br/>dalga kapı durumu / kontrol listesi"]
|
||||||
|
C4 --> T1["Checkout güncelle / ortam bilgisi / katalog<br/>gate'ler / bf-release / destek paketi / log"]
|
||||||
|
C5 --> D1["Kimlik / sağlık / durum / ağ / disk<br/>paketler / servisler / journal / envanter"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ek: İlk kurulum ve durum akışı
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
G["git clone<br/>Blueforce-Linux"] --> B["sudo admin/bf-bootstrap.sh<br/>(veya curl | sudo bash)"]
|
||||||
|
B --> M["sudo bf-menu"]
|
||||||
|
M --> ID["Kategori 1: bayi no gir<br/>ör. 12010193"]
|
||||||
|
ID --> INST["18/18 modül OK"]
|
||||||
|
INST --> PO["PROVISIONED_OFFLINE<br/>yerel kurulum bitti"]
|
||||||
|
PO -->|kategori 5: enrollment| EN["ENROLLED"]
|
||||||
|
EN -->|merkez kanal kanıtı| RDY["READY<br/>teslim edilebilir"]
|
||||||
|
B -. "ISO/firstboot yolu<br/>(ikincil, opsiyonel)" .-> FB["docs/26 / docs/27"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ek: Hızlı referans (menü ↔ komut)
|
||||||
|
|
||||||
|
| Menüde | Altında çalışan komut / playbook |
|
||||||
|
|---|---|
|
||||||
|
| 1 → 1 Kuru koşu | `blueforce-install.sh --dealer-id <8hane> --check` |
|
||||||
|
| 1 → 2 Kurulum | `blueforce-install.sh --dealer-id <8hane>` (`--offline`, `--resume` opsiyonel) |
|
||||||
|
| 1 → 4 Tek modül | `blueforce-install.sh --dealer-id <8hane> --only <modül>` |
|
||||||
|
| 1 → 5 / 6 Modül durumu / log | `/var/lib/blueforce/install-state`, `/var/log/blueforce-install.log` |
|
||||||
|
| 2.1 → 1 Ping | `bf-ping.yml` |
|
||||||
|
| 2.1 → 9 Field OS sürümü | `bf-fieldos-version.yml` |
|
||||||
|
| 2.2 → 7 Log topla | `bf-collect-logs.yml` |
|
||||||
|
| 2.2 → 9 Kontrollü reboot | `bf-reboot.yml` (release-gated, seri + sağlık kapısı) |
|
||||||
|
| 2.2 → 10 Onaylı güncelleme | `bf-deploy-update.yml` (`package=version` pinleri) |
|
||||||
|
| 3 → 1..5 Merkez hazırlığı | `ansible/inventory/group_vars/all.yml` denetimi + DNS/TCP probe |
|
||||||
|
| 4 → 4/5/6 Kapılar | `bf-check-local`, `bf-check-enrollment`, `bf-check-ready` |
|
||||||
|
| 4 → 8 Sürüm kimliği | `bf-release` |
|
||||||
|
| 4 → 10 Destek paketi | `bf-support-bundle --out-dir` |
|
||||||
|
| 5 → 2/3 Sağlık ve durum | `bf-status`, `/var/lib/blueforce/state.json`, `bf-enrollment-status` |
|
||||||
|
| Konsol dışı (opsiyonel) | `sudo bf-creds --generate` / `--fetch` |
|
||||||
|
| Denetim log'u | `/var/log/blueforce-console.log` (mod `600`) |
|
||||||
752
tests/check-admin-static.sh
Executable file
752
tests/check-admin-static.sh
Executable file
|
|
@ -0,0 +1,752 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Static + behavioural contract tests for the git-based bootstrap and the
|
||||||
|
# optional credential tool:
|
||||||
|
#
|
||||||
|
# admin/bf-bootstrap.sh one-command install from git
|
||||||
|
# admin/bf-creds optional, default-OFF credential generation/delivery
|
||||||
|
# admin/lib/creds.sh shared helpers
|
||||||
|
#
|
||||||
|
# Everything here is mock/sandboxed: no network, no real apt, no root, and no
|
||||||
|
# write outside a mktemp sandbox. The bootstrap apply path is exercised with
|
||||||
|
# `id` and `apt-get` shims placed first in PATH, every PATH entry providing
|
||||||
|
# ansible-playbook removed (so the dependency branch runs deterministically and
|
||||||
|
# still only reaches the apt-get shim), and BF_INSTALL_DIR / BF_BIN_DIR /
|
||||||
|
# BF_REPO_URL pointed at the sandbox — a real package install or a real
|
||||||
|
# /usr/local/bin link can never happen from this suite.
|
||||||
|
#
|
||||||
|
# Every load-bearing check is paired with a negative control: the same
|
||||||
|
# predicate is run against a deliberately broken copy and MUST trip, so a
|
||||||
|
# check that has quietly stopped testing anything fails the suite instead of
|
||||||
|
# passing silently.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
BOOTSTRAP="$REPO_ROOT/admin/bf-bootstrap.sh"
|
||||||
|
CREDS="$REPO_ROOT/admin/bf-creds"
|
||||||
|
CREDS_LIB="$REPO_ROOT/admin/lib/creds.sh"
|
||||||
|
GITIGNORE="$REPO_ROOT/.gitignore"
|
||||||
|
|
||||||
|
FAIL=0
|
||||||
|
CHECKS=0
|
||||||
|
fail() { printf 'FAIL: %s\n' "$*" >&2; FAIL=1; }
|
||||||
|
pass() { printf 'OK: %s\n' "$*"; }
|
||||||
|
note() { printf 'NOTE: %s\n' "$*"; }
|
||||||
|
check() { CHECKS=$((CHECKS + 1)); }
|
||||||
|
|
||||||
|
WORK="$(mktemp -d "${TMPDIR:-/tmp}/check-admin.XXXXXX")"
|
||||||
|
cleanup() { rm -rf "$WORK"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Predicates and assertion helpers.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
has() { [[ -f "$1" ]] && grep -qE -- "$2" "$1"; }
|
||||||
|
lacks() { [[ -f "$1" ]] && ! grep -qE -- "$2" "$1"; }
|
||||||
|
# Case-insensitive variants, used for the secret-material scan (a planted
|
||||||
|
# `DEMO_TOKEN=...` must be found just like a lowercase one).
|
||||||
|
has_i() { [[ -f "$1" ]] && grep -qiE -- "$2" "$1"; }
|
||||||
|
lacks_i() { [[ -f "$1" ]] && ! grep -qiE -- "$2" "$1"; }
|
||||||
|
|
||||||
|
# contract <desc> <file> <predicate> <regex> -- the predicate must hold.
|
||||||
|
contract() {
|
||||||
|
check
|
||||||
|
if "$3" "$2" "$4"; then
|
||||||
|
pass "$1"
|
||||||
|
else
|
||||||
|
fail "$1 [file=${2#"$REPO_ROOT"/} pattern=$4]"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# control <desc> <broken-file> <predicate> <regex> -- the predicate must NOT hold.
|
||||||
|
control() {
|
||||||
|
check
|
||||||
|
if "$3" "$2" "$4"; then
|
||||||
|
fail "negative control did not trip: $1"
|
||||||
|
else
|
||||||
|
pass "negative control trips: $1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_rc() { # <desc> <expected-rc> <actual-rc>
|
||||||
|
check
|
||||||
|
if [[ "$2" -eq "$3" ]]; then
|
||||||
|
pass "$1"
|
||||||
|
else
|
||||||
|
fail "$1 (expected exit $2, got $3)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_absent() { # <desc> <path>
|
||||||
|
check
|
||||||
|
if [[ ! -e "$2" ]]; then
|
||||||
|
pass "$1"
|
||||||
|
else
|
||||||
|
fail "$1 (path exists: $2)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_present() { # <desc> <path>
|
||||||
|
check
|
||||||
|
if [[ -e "$2" ]]; then
|
||||||
|
pass "$1"
|
||||||
|
else
|
||||||
|
fail "$1 (path missing: $2)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_mode() { # <desc> <path> <mode>
|
||||||
|
check
|
||||||
|
local mode=""
|
||||||
|
mode="$(stat -c '%a' "$2" 2>/dev/null || true)"
|
||||||
|
if [[ "$mode" == "$3" ]]; then
|
||||||
|
pass "$1"
|
||||||
|
else
|
||||||
|
fail "$1 (mode is '${mode:-missing}', expected $3)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_contains() { # <desc> <file> <literal>
|
||||||
|
check
|
||||||
|
if grep -qF -- "$3" "$2" 2>/dev/null; then
|
||||||
|
pass "$1"
|
||||||
|
else
|
||||||
|
fail "$1 (literal not found: $3)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_absent_text() { # <desc> <file> <literal>
|
||||||
|
check
|
||||||
|
if grep -qF -- "$3" "$2" 2>/dev/null; then
|
||||||
|
fail "$1 (literal found: $3)"
|
||||||
|
else
|
||||||
|
pass "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# secret_leaked <output-file> <secret> -- 0 when the secret is visible.
|
||||||
|
# This is the leak detector used for the generated credential; its negative
|
||||||
|
# control below proves it can actually find a secret.
|
||||||
|
secret_leaked() { grep -qF -- "$2" "$1" 2>/dev/null; }
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 1. Presence, shebang, strict mode, syntax.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
for target in "$BOOTSTRAP" "$CREDS" "$CREDS_LIB"; do
|
||||||
|
check
|
||||||
|
if [[ -f "$target" ]]; then
|
||||||
|
pass "present: ${target#"$REPO_ROOT"/}"
|
||||||
|
else
|
||||||
|
fail "missing: ${target#"$REPO_ROOT"/}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for target in "$BOOTSTRAP" "$CREDS" "$CREDS_LIB"; do
|
||||||
|
[[ -f "$target" ]] || continue
|
||||||
|
name="${target#"$REPO_ROOT"/}"
|
||||||
|
check
|
||||||
|
if [[ "$(head -n 1 "$target")" == '#!/usr/bin/env bash' ]]; then
|
||||||
|
pass "bash shebang: $name"
|
||||||
|
else
|
||||||
|
fail "bash shebang missing: $name"
|
||||||
|
fi
|
||||||
|
check
|
||||||
|
if grep -q '^set -euo pipefail$' "$target"; then
|
||||||
|
pass "strict mode (set -euo pipefail): $name"
|
||||||
|
else
|
||||||
|
fail "strict mode missing: $name"
|
||||||
|
fi
|
||||||
|
check
|
||||||
|
if bash -n "$target" 2>/dev/null; then
|
||||||
|
pass "bash parses: $name"
|
||||||
|
else
|
||||||
|
fail "bash parse error: $name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# The two entry points are invoked directly and symlinked into /usr/local/bin.
|
||||||
|
for target in "$BOOTSTRAP" "$CREDS"; do
|
||||||
|
[[ -f "$target" ]] || continue
|
||||||
|
check
|
||||||
|
if [[ -x "$target" ]]; then
|
||||||
|
pass "executable bit: ${target#"$REPO_ROOT"/}"
|
||||||
|
else
|
||||||
|
fail "not executable: ${target#"$REPO_ROOT"/}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Negative control for the syntax gate: a broken script must be rejected.
|
||||||
|
printf '#!/usr/bin/env bash\nif true; then\n' > "$WORK/broken.sh"
|
||||||
|
check
|
||||||
|
if bash -n "$WORK/broken.sh" 2>/dev/null; then
|
||||||
|
fail 'negative control did not trip: bash -n accepted broken syntax'
|
||||||
|
else
|
||||||
|
pass 'negative control trips: bash -n rejects broken syntax'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 2. --help works and documents the documented interface.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
check
|
||||||
|
if bash "$BOOTSTRAP" --help > "$WORK/bootstrap-help.out" 2>&1; then
|
||||||
|
pass 'bf-bootstrap.sh --help exits 0'
|
||||||
|
else
|
||||||
|
fail 'bf-bootstrap.sh --help failed'
|
||||||
|
fi
|
||||||
|
for flag in '--check' '--dir' '--branch' '--yes'; do
|
||||||
|
contract "bf-bootstrap --help documents $flag" "$WORK/bootstrap-help.out" has "\\${flag}"
|
||||||
|
done
|
||||||
|
|
||||||
|
check
|
||||||
|
if bash "$CREDS" --help > "$WORK/creds-help.out" 2>&1; then
|
||||||
|
pass 'bf-creds --help exits 0'
|
||||||
|
else
|
||||||
|
fail 'bf-creds --help failed'
|
||||||
|
fi
|
||||||
|
for flag in '--generate' '--fetch' '--repo' '--dealer-id'; do
|
||||||
|
contract "bf-creds --help documents $flag" "$WORK/creds-help.out" has "\\${flag}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Unknown arguments are refused with exit 2 (and never mutate anything).
|
||||||
|
set +e
|
||||||
|
bash "$BOOTSTRAP" --definitely-not-a-flag > "$WORK/bootstrap-bad.out" 2>&1
|
||||||
|
bootstrap_bad_rc=$?
|
||||||
|
bash "$CREDS" --definitely-not-a-flag > "$WORK/creds-bad.out" 2>&1
|
||||||
|
creds_bad_rc=$?
|
||||||
|
set -e
|
||||||
|
expect_rc 'bf-bootstrap.sh rejects an unknown argument with exit 2' 2 "$bootstrap_bad_rc"
|
||||||
|
expect_rc 'bf-creds rejects an unknown argument with exit 2' 2 "$creds_bad_rc"
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 3. bf-bootstrap.sh static contracts (+ negative controls).
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
contract 'bf-bootstrap checks for root before mutating' "$BOOTSTRAP" has 'id -u'
|
||||||
|
contract 'bf-bootstrap installs prerequisites with apt-get' "$BOOTSTRAP" has 'apt-get install'
|
||||||
|
contract 'bf-bootstrap prepares git/curl/ansible-core' "$BOOTSTRAP" has 'ansible-playbook'
|
||||||
|
contract 'bf-bootstrap clones the repository' "$BOOTSTRAP" has 'git clone'
|
||||||
|
contract 'bf-bootstrap updates an existing checkout fast-forward only' "$BOOTSTRAP" has 'merge --ff-only'
|
||||||
|
contract 'bf-bootstrap never rewrites the checkout history' "$BOOTSTRAP" lacks 'reset --hard|git clean'
|
||||||
|
contract 'bf-bootstrap links tools into /usr/local/bin' "$BOOTSTRAP" has '/usr/local/bin'
|
||||||
|
contract 'bf-bootstrap links idempotently' "$BOOTSTRAP" has 'ln -sfn'
|
||||||
|
contract 'bf-bootstrap discovers the admin tools' "$BOOTSTRAP" has 'admin/bf-\*'
|
||||||
|
contract 'bf-bootstrap discovers the diagnostics tools' "$BOOTSTRAP" has 'scripts/diagnostics/bf-\*'
|
||||||
|
contract 'bf-bootstrap discovers the maintenance tools' "$BOOTSTRAP" has 'scripts/maintenance/bf-\*'
|
||||||
|
contract 'bf-bootstrap wires up bf-menu' "$BOOTSTRAP" has 'bf-menu'
|
||||||
|
contract 'bf-bootstrap supports Ubuntu 24.04' "$BOOTSTRAP" has '24\.04'
|
||||||
|
contract 'bf-bootstrap supports Ubuntu 26.04' "$BOOTSTRAP" has '26\.04'
|
||||||
|
contract 'bf-bootstrap has a read-only check mode' "$BOOTSTRAP" has 'CHECK_MODE'
|
||||||
|
contract 'bf-bootstrap prints the completion contract' "$BOOTSTRAP" has 'Kurulum tamam\. Başlamak için: sudo bf-menu'
|
||||||
|
contract 'bf-bootstrap carries no private key material' "$BOOTSTRAP" lacks 'BEGIN [A-Z ]*PRIVATE KEY'
|
||||||
|
contract 'bf-bootstrap generates no WireGuard key' "$BOOTSTRAP" lacks 'wg genkey'
|
||||||
|
|
||||||
|
# Negative controls: break each load-bearing property and prove the check trips.
|
||||||
|
sed -E 's#/usr/local/bin#/usr/bin#g' "$BOOTSTRAP" > "$WORK/b-ins-local-bin.sh"
|
||||||
|
chmod 0755 "$WORK/b-ins-local-bin.sh"
|
||||||
|
control 'bf-bootstrap target directory is checked' "$WORK/b-ins-local-bin.sh" has '/usr/local/bin'
|
||||||
|
|
||||||
|
sed -E 's#merge --ff-only#reset --hard#g' "$BOOTSTRAP" > "$WORK/b-reset.sh"
|
||||||
|
chmod 0755 "$WORK/b-reset.sh"
|
||||||
|
control 'bf-bootstrap fast-forward requirement is checked' "$WORK/b-reset.sh" has 'merge --ff-only'
|
||||||
|
control 'bf-bootstrap history-rewrite ban is checked' "$WORK/b-reset.sh" lacks 'reset --hard|git clean'
|
||||||
|
|
||||||
|
sed -E 's#id -u#true#g' "$BOOTSTRAP" > "$WORK/b-no-root.sh"
|
||||||
|
chmod 0755 "$WORK/b-no-root.sh"
|
||||||
|
control 'bf-bootstrap root check is checked' "$WORK/b-no-root.sh" has 'id -u'
|
||||||
|
|
||||||
|
sed -E '/Kurulum tamam/d' "$BOOTSTRAP" > "$WORK/b-no-summary.sh"
|
||||||
|
chmod 0755 "$WORK/b-no-summary.sh"
|
||||||
|
control 'bf-bootstrap completion contract is checked' "$WORK/b-no-summary.sh" has 'Kurulum tamam\. Başlamak için: sudo bf-menu'
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 4. bf-bootstrap.sh runtime: --check is mutation-free, apply mode works.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# A sandbox PATH shim: `id` reports uid 0 (so the apply path can run unprivileged
|
||||||
|
# inside the sandbox) and `apt-get` only records the call. Nothing else changes.
|
||||||
|
SHIM="$WORK/shims"
|
||||||
|
mkdir -p "$SHIM"
|
||||||
|
cat > "$SHIM/id" <<'SHIMEOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
if [[ "${1:-}" == "-u" ]]; then printf '0\n'; exit 0; fi
|
||||||
|
printf 'uid=0(root) gid=0(root) groups=0(root)\n'
|
||||||
|
SHIMEOF
|
||||||
|
cat > "$SHIM/apt-get" <<'SHIMEOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf 'shim apt-get: %s\n' "$*" >> "${BF_SHIM_APT_LOG:-/dev/null}"
|
||||||
|
exit 0
|
||||||
|
SHIMEOF
|
||||||
|
chmod 0755 "$SHIM/id" "$SHIM/apt-get"
|
||||||
|
|
||||||
|
# Deterministic dependency-install branch: drop every PATH entry that provides
|
||||||
|
# ansible-playbook, so the bootstrap really sees a missing prerequisite and
|
||||||
|
# takes its apt-get path (against the shim, never the real package manager).
|
||||||
|
# git and curl must stay resolvable; when they would be filtered out too, the
|
||||||
|
# unmodified PATH is kept and the assertion below falls back to a note.
|
||||||
|
PATH_NO_ANSIBLE=""
|
||||||
|
while IFS= read -r path_dir; do
|
||||||
|
[[ -z "$path_dir" ]] && continue
|
||||||
|
[[ -x "$path_dir/ansible-playbook" ]] && continue
|
||||||
|
PATH_NO_ANSIBLE="${PATH_NO_ANSIBLE:+$PATH_NO_ANSIBLE:}$path_dir"
|
||||||
|
done < <(printf '%s\n' "$PATH" | tr ':' '\n')
|
||||||
|
APPLY_PATH="$SHIM:$PATH_NO_ANSIBLE"
|
||||||
|
if ! PATH="$APPLY_PATH" command -v git >/dev/null 2>&1 || ! PATH="$APPLY_PATH" command -v curl >/dev/null 2>&1; then
|
||||||
|
APPLY_PATH="$SHIM:$PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
bootstrap_apply() { # <checkout-dir> <bin-dir> <out-file> <apt-log>
|
||||||
|
PATH="$APPLY_PATH" \
|
||||||
|
BF_SHIM_APT_LOG="$4" \
|
||||||
|
BF_BIN_DIR="$2" \
|
||||||
|
BF_INSTALL_DIR="$1" \
|
||||||
|
BF_REPO_URL="$REPO_ROOT" \
|
||||||
|
bash "$BOOTSTRAP" --yes < /dev/null > "$3" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4a. --check must create nothing at all, even under a full sandbox prefix.
|
||||||
|
CHECK_ROOT="$WORK/check-root"
|
||||||
|
set +e
|
||||||
|
PATH="$APPLY_PATH" \
|
||||||
|
BF_BIN_DIR="$CHECK_ROOT/bin" \
|
||||||
|
BF_INSTALL_DIR="$CHECK_ROOT/checkout" \
|
||||||
|
BF_REPO_URL="$REPO_ROOT" \
|
||||||
|
bash "$BOOTSTRAP" --check < /dev/null > "$WORK/check.out" 2>&1
|
||||||
|
check_rc=$?
|
||||||
|
set -e
|
||||||
|
expect_rc 'bf-bootstrap --check exits 0' 0 "$check_rc"
|
||||||
|
expect_absent 'bf-bootstrap --check creates no checkout' "$CHECK_ROOT/checkout"
|
||||||
|
expect_absent 'bf-bootstrap --check creates no bin directory' "$CHECK_ROOT/bin"
|
||||||
|
expect_absent 'bf-bootstrap --check creates nothing under its target prefix' "$CHECK_ROOT"
|
||||||
|
expect_contains 'bf-bootstrap --check reports that nothing was changed' "$WORK/check.out" 'no changes were made'
|
||||||
|
if PATH="$APPLY_PATH" command -v ansible-playbook >/dev/null 2>&1; then
|
||||||
|
note 'ansible-playbook resolves even after PATH filtering: the dependency-plan assertion is limited to the static contract'
|
||||||
|
else
|
||||||
|
expect_contains 'check mode reports the dependency install it would perform' "$WORK/check.out" 'would install with apt-get'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4b. --check against the real checkout must install nothing and must discover
|
||||||
|
# the admin tools (this is the only non-sandboxed run in the suite).
|
||||||
|
local_bin_before="$(ls -1 /usr/local/bin 2>/dev/null | sort | tr '\n' ' ')"
|
||||||
|
mode_before="$(stat -c '%a' "$BOOTSTRAP" "$CREDS" 2>/dev/null | tr '\n' ' ')"
|
||||||
|
set +e
|
||||||
|
bash "$BOOTSTRAP" --check < /dev/null > "$WORK/check-repo.out" 2>&1
|
||||||
|
check_repo_rc=$?
|
||||||
|
set -e
|
||||||
|
expect_rc 'bf-bootstrap --check runs unprivileged against the checkout' 0 "$check_repo_rc"
|
||||||
|
expect_contains 'check mode reports admin/bf-bootstrap.sh as a tool' "$WORK/check-repo.out" 'admin/bf-bootstrap.sh'
|
||||||
|
expect_contains 'check mode reports admin/bf-creds as a tool' "$WORK/check-repo.out" 'admin/bf-creds'
|
||||||
|
expect_contains 'check mode reports a diagnostics tool' "$WORK/check-repo.out" 'scripts/diagnostics/bf-status'
|
||||||
|
check
|
||||||
|
if [[ "$local_bin_before" == "$(ls -1 /usr/local/bin 2>/dev/null | sort | tr '\n' ' ')" ]]; then
|
||||||
|
pass 'bf-bootstrap --check installs nothing into /usr/local/bin'
|
||||||
|
else
|
||||||
|
fail 'bf-bootstrap --check wrote into /usr/local/bin'
|
||||||
|
fi
|
||||||
|
check
|
||||||
|
if [[ "$mode_before" == "$(stat -c '%a' "$BOOTSTRAP" "$CREDS" 2>/dev/null | tr '\n' ' ')" ]]; then
|
||||||
|
pass 'bf-bootstrap --check leaves tool modes unchanged'
|
||||||
|
else
|
||||||
|
fail 'bf-bootstrap --check changed tool modes'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4c. Harness self-test: prove the "nothing was created" assertion can detect a
|
||||||
|
# mutation, so 4a is not a vacuous pass. A synthetic probe that only creates
|
||||||
|
# its target when check mode is bypassed must trip the very same assertion.
|
||||||
|
PROBE="$WORK/probe.sh"
|
||||||
|
cat > "$PROBE" <<'PROBEEOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ "${1:-}" == "--check" ]]; then
|
||||||
|
printf 'check only\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
mkdir -p "$2"
|
||||||
|
printf 'created\n' > "$2/marker"
|
||||||
|
PROBEEOF
|
||||||
|
probe_ok="$WORK/probe-ok"
|
||||||
|
set +e
|
||||||
|
bash "$PROBE" --check "$probe_ok" >/dev/null 2>&1
|
||||||
|
set -e
|
||||||
|
expect_absent 'harness self-test: the probe creates nothing in check mode' "$probe_ok"
|
||||||
|
sed -E 's#--check#--never#g' "$PROBE" > "$WORK/probe-broken.sh"
|
||||||
|
probe_broken="$WORK/probe-broken"
|
||||||
|
set +e
|
||||||
|
bash "$WORK/probe-broken.sh" --check "$probe_broken" >/dev/null 2>&1
|
||||||
|
set -e
|
||||||
|
expect_present 'harness self-test control: the same assertion detects a mutation' "$probe_broken/marker"
|
||||||
|
|
||||||
|
# 4d. Apply mode in the sandbox (fresh clone path).
|
||||||
|
APPLY_ROOT="$WORK/apply-root"
|
||||||
|
mkdir -p "$APPLY_ROOT"
|
||||||
|
set +e
|
||||||
|
bootstrap_apply "$APPLY_ROOT/checkout" "$APPLY_ROOT/bin" "$WORK/apply1.out" "$WORK/apt1.log"
|
||||||
|
apply_rc=$?
|
||||||
|
set -e
|
||||||
|
expect_rc 'bf-bootstrap apply mode exits 0 in the sandbox' 0 "$apply_rc"
|
||||||
|
expect_present 'bf-bootstrap cloned the repository' "$APPLY_ROOT/checkout/.git"
|
||||||
|
expect_present 'bf-bootstrap linked a diagnostics tool' "$APPLY_ROOT/bin/bf-status"
|
||||||
|
expect_contains 'bf-bootstrap prints the completion contract' "$WORK/apply1.out" 'Kurulum tamam. Başlamak için: sudo bf-menu'
|
||||||
|
expect_contains 'bf-bootstrap reports the repository in its summary' "$WORK/apply1.out" "$APPLY_ROOT/checkout"
|
||||||
|
expect_contains 'bf-bootstrap warns when bf-menu is not in the clone' "$WORK/apply1.out" 'admin/bf-menu is not present'
|
||||||
|
linked1="$(find "$APPLY_ROOT/bin" -maxdepth 1 -type l 2>/dev/null | wc -l | tr -d ' ')"
|
||||||
|
check
|
||||||
|
if [[ "$linked1" -ge 10 ]]; then
|
||||||
|
pass "bf-bootstrap linked the tool set ($linked1 links)"
|
||||||
|
else
|
||||||
|
fail "bf-bootstrap linked too few tools ($linked1 links)"
|
||||||
|
fi
|
||||||
|
# The dependency branch runs against the apt-get shim, never the real apt.
|
||||||
|
if PATH="$APPLY_PATH" command -v ansible-playbook >/dev/null 2>&1; then
|
||||||
|
note 'ansible-playbook resolves even after PATH filtering: the dependency-install assertion is skipped'
|
||||||
|
else
|
||||||
|
expect_contains 'dependency install goes through apt' "$WORK/apt1.log" 'install -y --no-install-recommends'
|
||||||
|
expect_contains 'the package list names the missing prerequisite' "$WORK/apt1.log" 'ansible-core'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4e. Idempotency: a second apply run must succeed and keep the same links.
|
||||||
|
set +e
|
||||||
|
bootstrap_apply "$APPLY_ROOT/checkout" "$APPLY_ROOT/bin" "$WORK/apply2.out" "$WORK/apt2.log"
|
||||||
|
apply2_rc=$?
|
||||||
|
set -e
|
||||||
|
expect_rc 'bf-bootstrap apply mode is idempotent on re-run' 0 "$apply2_rc"
|
||||||
|
linked2="$(find "$APPLY_ROOT/bin" -maxdepth 1 -type l 2>/dev/null | wc -l | tr -d ' ')"
|
||||||
|
check
|
||||||
|
if [[ "$linked1" == "$linked2" ]]; then
|
||||||
|
pass "re-run keeps the same tool set ($linked2 links)"
|
||||||
|
else
|
||||||
|
fail "re-run changed the tool set ($linked1 -> $linked2 links)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4f. An existing checkout is refreshed, not reset: local changes are reported
|
||||||
|
# and preserved, and admin/bf-* tools are linked from it.
|
||||||
|
SEEDED="$WORK/seeded"
|
||||||
|
git clone --quiet "$REPO_ROOT" "$SEEDED"
|
||||||
|
cp -a "$REPO_ROOT/admin" "$SEEDED/admin"
|
||||||
|
set +e
|
||||||
|
bootstrap_apply "$SEEDED" "$WORK/seeded-bin" "$WORK/apply3.out" "$WORK/apt3.log"
|
||||||
|
apply3_rc=$?
|
||||||
|
set -e
|
||||||
|
expect_rc 'bf-bootstrap apply mode accepts an existing checkout' 0 "$apply3_rc"
|
||||||
|
expect_present 'bf-bootstrap linked admin/bf-bootstrap.sh' "$WORK/seeded-bin/bf-bootstrap.sh"
|
||||||
|
expect_present 'bf-bootstrap linked admin/bf-creds' "$WORK/seeded-bin/bf-creds"
|
||||||
|
expect_contains 'bf-bootstrap reports local changes instead of resetting them' "$WORK/apply3.out" 'local changes present'
|
||||||
|
expect_present 'bf-bootstrap left the existing checkout intact' "$SEEDED/admin/bf-creds"
|
||||||
|
|
||||||
|
# 4g. Apply mode without root must refuse before touching anything.
|
||||||
|
if [[ "$(id -u)" -ne 0 ]]; then
|
||||||
|
ROOT_TARGET="$WORK/root-target"
|
||||||
|
set +e
|
||||||
|
BF_BIN_DIR="$WORK/root-bin" \
|
||||||
|
BF_INSTALL_DIR="$ROOT_TARGET" \
|
||||||
|
BF_REPO_URL="$REPO_ROOT" \
|
||||||
|
bash "$BOOTSTRAP" --yes < /dev/null > "$WORK/root.out" 2>&1
|
||||||
|
root_rc=$?
|
||||||
|
set -e
|
||||||
|
expect_rc 'bf-bootstrap refuses to apply without root' 1 "$root_rc"
|
||||||
|
expect_contains 'bf-bootstrap explains the root requirement' "$WORK/root.out" 'must run as root'
|
||||||
|
expect_absent 'the refused run created no checkout' "$ROOT_TARGET"
|
||||||
|
expect_absent 'the refused run created no bin directory' "$WORK/root-bin"
|
||||||
|
else
|
||||||
|
note 'running as root: the unprivileged refusal control is skipped'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4h. A target directory that is not a git checkout must be refused untouched.
|
||||||
|
NOT_A_REPO="$WORK/not-a-repo"
|
||||||
|
mkdir -p "$NOT_A_REPO"
|
||||||
|
printf 'keep me\n' > "$NOT_A_REPO/precious.txt"
|
||||||
|
set +e
|
||||||
|
PATH="$SHIM:$PATH" \
|
||||||
|
BF_BIN_DIR="$WORK/not-a-repo-bin" \
|
||||||
|
BF_INSTALL_DIR="$NOT_A_REPO" \
|
||||||
|
BF_REPO_URL="$REPO_ROOT" \
|
||||||
|
bash "$BOOTSTRAP" --yes < /dev/null > "$WORK/not-a-repo.out" 2>&1
|
||||||
|
not_a_repo_rc=$?
|
||||||
|
set -e
|
||||||
|
expect_rc 'bf-bootstrap refuses a directory that is not a git checkout' 1 "$not_a_repo_rc"
|
||||||
|
expect_contains 'the refusal is explained' "$WORK/not-a-repo.out" 'not a git checkout'
|
||||||
|
expect_present 'the foreign directory was left untouched' "$NOT_A_REPO/precious.txt"
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 5. bf-creds static contracts (+ negative controls).
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
contract 'bf-creds is default-OFF by default' "$CREDS" has 'BF_GENERATE_CREDENTIALS:-0'
|
||||||
|
contract 'bf-creds has no default-ON regression' "$CREDS" lacks 'BF_GENERATE_CREDENTIALS:-1'
|
||||||
|
contract 'bf-creds accepts the explicit --generate opt-in' "$CREDS" has 'GENERATE=1'
|
||||||
|
contract 'bf-creds accepts the explicit --fetch opt-in' "$CREDS" has 'FETCH=1'
|
||||||
|
contract 'bf-creds marks generation disabled by default' "$CREDS" has 'DISABLED'
|
||||||
|
contract 'bf-creds never enables shell tracing' "$CREDS" lacks 'set -x'
|
||||||
|
contract 'bf-creds writes the record at mode 0600' "$CREDS_LIB" has 'BF_CREDENTIALS_MODE:=600'
|
||||||
|
contract 'bf-creds keeps the delivery path at admin/secrets' "$CREDS_LIB" has 'BF_SECRETS_SUBDIR:=admin/secrets'
|
||||||
|
contract 'bf-creds reports the WireGuard private key path only' "$CREDS" has 'private_key_path'
|
||||||
|
contract 'bf-creds documents that the SSH decision is unchanged' "$CREDS" has 'PasswordAuthentication no'
|
||||||
|
contract 'creds.sh forces 0600 on the written file' "$CREDS_LIB" has 'install -m "\$BF_CREDENTIALS_MODE"'
|
||||||
|
contract 'creds.sh forces 0700 on the delivery directory' "$CREDS_LIB" has 'install -d -m "\$BF_SECRETS_DIR_MODE"'
|
||||||
|
contract 'creds.sh refuses to write through a symlink' "$CREDS_LIB" has 'refusing to write through a symlink'
|
||||||
|
contract 'creds.sh never enables shell tracing' "$CREDS_LIB" lacks 'set -x'
|
||||||
|
|
||||||
|
# Mutation builder for bf-creds copies: the tool sources lib/creds.sh relative
|
||||||
|
# to its own real path, so a mutant needs a lib/ directory beside it.
|
||||||
|
creds_mutant() { # <name> <awk-program-file>
|
||||||
|
local dir="$WORK/mut-$1"
|
||||||
|
mkdir -p "$dir/lib"
|
||||||
|
ln -sfn "$CREDS_LIB" "$dir/lib/creds.sh"
|
||||||
|
awk -f "$2" "$CREDS" > "$dir/bf-creds"
|
||||||
|
chmod 0755 "$dir/bf-creds"
|
||||||
|
printf '%s' "$dir/bf-creds"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Negative control: a default-ON variant must trip both default-off checks.
|
||||||
|
cat > "$WORK/default-on.awk" <<'AWKEOF'
|
||||||
|
{ gsub(/BF_GENERATE_CREDENTIALS:-0/, "BF_GENERATE_CREDENTIALS:-1"); print }
|
||||||
|
AWKEOF
|
||||||
|
DEFAULT_ON="$(creds_mutant default-on "$WORK/default-on.awk")"
|
||||||
|
control 'bf-creds default-off contract is checked' "$DEFAULT_ON" has 'BF_GENERATE_CREDENTIALS:-0'
|
||||||
|
control 'bf-creds default-ON ban is checked' "$DEFAULT_ON" lacks 'BF_GENERATE_CREDENTIALS:-1'
|
||||||
|
|
||||||
|
# Negative control: shell tracing (which would print secrets) must trip the ban.
|
||||||
|
mkdir -p "$WORK/mut-xtrace/lib"
|
||||||
|
ln -sfn "$CREDS_LIB" "$WORK/mut-xtrace/lib/creds.sh"
|
||||||
|
awk '{ print } END { print "set -x" }' "$CREDS" > "$WORK/mut-xtrace/bf-creds"
|
||||||
|
chmod 0755 "$WORK/mut-xtrace/bf-creds"
|
||||||
|
control 'bf-creds shell-tracing ban is checked' "$WORK/mut-xtrace/bf-creds" lacks 'set -x'
|
||||||
|
|
||||||
|
# Negative control: a leak of the generated secret must trip the leak detector.
|
||||||
|
cat > "$WORK/leak.awk" <<'AWKEOF'
|
||||||
|
/cannot generate a random secret/ {
|
||||||
|
print
|
||||||
|
print " printf \"leaked-credential=%s\\n\" \"$password\""
|
||||||
|
next
|
||||||
|
}
|
||||||
|
{ print }
|
||||||
|
AWKEOF
|
||||||
|
LEAK_MUTANT="$(creds_mutant leak "$WORK/leak.awk")"
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 6. bf-creds runtime in a sandbox (identity, default-off, generate, fetch).
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
CSB="$WORK/creds-sandbox"
|
||||||
|
mkdir -p "$CSB/wg"
|
||||||
|
FAKE_WG_PRIVATE='FAKEWGPRIVATE0000000000000000000000000000000='
|
||||||
|
FAKE_WG_PUBLIC='FAKEWGPUBLIC1111111111111111111111111111111='
|
||||||
|
FAKE_RUSTDESK='987654321'
|
||||||
|
printf '%s\n' "$FAKE_WG_PUBLIC" > "$CSB/wg/publickey"
|
||||||
|
printf '%s\n' "$FAKE_WG_PRIVATE" > "$CSB/wg/privatekey"
|
||||||
|
chmod 0600 "$CSB/wg/privatekey"
|
||||||
|
printf '%s\n' "$FAKE_RUSTDESK" > "$CSB/rustdesk-id"
|
||||||
|
printf '{ "phase": "PROVISIONED_OFFLINE", "device_id": "BF-12345678" }\n' > "$CSB/state.json"
|
||||||
|
|
||||||
|
CREDS_FILE="$CSB/credentials.txt"
|
||||||
|
CREDS_STATE_FILE="$CSB/state.json"
|
||||||
|
CREDS_DEVICE='BF-12345678'
|
||||||
|
|
||||||
|
# creds_env <script> <out-file> [args...]
|
||||||
|
creds_env() {
|
||||||
|
local script="$1" out="$2"
|
||||||
|
shift 2
|
||||||
|
BF_STATE_FILE="${CREDS_STATE_FILE:-$CSB/state.json}" \
|
||||||
|
BF_DEVICE="${CREDS_DEVICE-}" \
|
||||||
|
BF_WG_DIR="$CSB/wg" \
|
||||||
|
BF_RUSTDESK_ID_FILE="$CSB/rustdesk-id" \
|
||||||
|
BF_CREDENTIALS_FILE="$CREDS_FILE" \
|
||||||
|
bash "$script" "$@" > "$out" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 6a. Default behaviour: report only, no generation, and it says so.
|
||||||
|
CREDS_FILE="$CSB/default-run.txt"
|
||||||
|
set +e
|
||||||
|
creds_env "$CREDS" "$WORK/creds-default.out"
|
||||||
|
default_rc=$?
|
||||||
|
set -e
|
||||||
|
expect_rc 'bf-creds default run exits 0' 0 "$default_rc"
|
||||||
|
expect_absent 'bf-creds default run generates no credential file' "$CREDS_FILE"
|
||||||
|
expect_contains 'bf-creds states that generation is disabled by default' "$WORK/creds-default.out" 'DISABLED'
|
||||||
|
expect_contains 'bf-creds states that no password was generated' "$WORK/creds-default.out" 'no password was generated'
|
||||||
|
expect_absent_text 'bf-creds default run prints no credential line' "$WORK/creds-default.out" 'local_administrator_password'
|
||||||
|
expect_contains 'bf-creds reports the device identity' "$WORK/creds-default.out" 'device_id : BF-12345678'
|
||||||
|
expect_contains 'bf-creds reports the WireGuard public key' "$WORK/creds-default.out" "$FAKE_WG_PUBLIC"
|
||||||
|
expect_contains 'bf-creds reports the private key path' "$WORK/creds-default.out" "$CSB/wg/privatekey"
|
||||||
|
expect_contains 'bf-creds reports the RustDesk ID' "$WORK/creds-default.out" "$FAKE_RUSTDESK"
|
||||||
|
|
||||||
|
# Negative control for the default-off behaviour: the default-ON mutant must
|
||||||
|
# create the credential file without any flag, proving 6a is not vacuous.
|
||||||
|
CREDS_FILE="$WORK/default-on-run.txt"
|
||||||
|
set +e
|
||||||
|
creds_env "$DEFAULT_ON" "$WORK/creds-default-on.out"
|
||||||
|
set -e
|
||||||
|
expect_present 'control: the default-ON mutant generates without a flag' "$CREDS_FILE"
|
||||||
|
|
||||||
|
# 6b. --generate writes the record, and the secret never reaches stdout.
|
||||||
|
CREDS_FILE="$CSB/credentials.txt"
|
||||||
|
set +e
|
||||||
|
creds_env "$CREDS" "$WORK/creds-generate.out" --generate
|
||||||
|
generate_rc=$?
|
||||||
|
set -e
|
||||||
|
expect_rc 'bf-creds --generate exits 0' 0 "$generate_rc"
|
||||||
|
expect_present 'bf-creds --generate writes the credential record' "$CREDS_FILE"
|
||||||
|
expect_mode 'the credential record is mode 600' "$CREDS_FILE" 600
|
||||||
|
expect_contains 'the record carries the device id' "$CREDS_FILE" 'device_id: BF-12345678'
|
||||||
|
expect_contains 'the record carries the dealer number' "$CREDS_FILE" 'dealer_number: 12345678'
|
||||||
|
expect_contains 'the record carries the hostname' "$CREDS_FILE" 'hostname:'
|
||||||
|
expect_contains 'the record carries the creation time' "$CREDS_FILE" 'generated_at:'
|
||||||
|
expect_contains 'the record carries the WireGuard public key' "$CREDS_FILE" "$FAKE_WG_PUBLIC"
|
||||||
|
expect_contains 'the record carries the private key path' "$CREDS_FILE" "$CSB/wg/privatekey"
|
||||||
|
expect_contains 'the record states the private key is not recorded' "$CREDS_FILE" 'NOT RECORDED'
|
||||||
|
expect_absent_text 'the record never contains the WireGuard private key value' "$CREDS_FILE" "$FAKE_WG_PRIVATE"
|
||||||
|
expect_absent_text 'stdout never contains the WireGuard private key value' "$WORK/creds-generate.out" "$FAKE_WG_PRIVATE"
|
||||||
|
expect_contains 'stdout names the credential record' "$WORK/creds-generate.out" "$CREDS_FILE"
|
||||||
|
expect_contains 'stdout confirms the record was written' "$WORK/creds-generate.out" 'written'
|
||||||
|
|
||||||
|
GENERATED_PASSWORD="$(sed -n 's/^local_administrator_password:[[:space:]]*//p' "$CREDS_FILE" | head -n 1)"
|
||||||
|
check
|
||||||
|
if [[ ${#GENERATED_PASSWORD} -ge 16 ]]; then
|
||||||
|
pass "the generated password is long enough (${#GENERATED_PASSWORD} characters)"
|
||||||
|
else
|
||||||
|
fail "the generated password is too short (${#GENERATED_PASSWORD} characters)"
|
||||||
|
fi
|
||||||
|
check
|
||||||
|
if secret_leaked "$WORK/creds-generate.out" "$GENERATED_PASSWORD"; then
|
||||||
|
fail 'the generated password leaked to stdout'
|
||||||
|
else
|
||||||
|
pass 'the generated password never reaches stdout'
|
||||||
|
fi
|
||||||
|
check
|
||||||
|
if secret_leaked "$WORK/creds-generate.out" "$FAKE_WG_PRIVATE"; then
|
||||||
|
fail 'the WireGuard private key leaked to stdout'
|
||||||
|
else
|
||||||
|
pass 'the WireGuard private key never reaches stdout'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Negative control for the leak detector: the leak mutant must be detected.
|
||||||
|
CREDS_FILE="$WORK/leak-run.txt"
|
||||||
|
set +e
|
||||||
|
creds_env "$LEAK_MUTANT" "$WORK/creds-leak.out" --generate
|
||||||
|
set -e
|
||||||
|
LEAKED_PASSWORD="$(sed -n 's/^local_administrator_password:[[:space:]]*//p' "$CREDS_FILE" | head -n 1)"
|
||||||
|
check
|
||||||
|
if [[ -n "$LEAKED_PASSWORD" ]] && secret_leaked "$WORK/creds-leak.out" "$LEAKED_PASSWORD"; then
|
||||||
|
pass 'control: the leak detector finds a secret that is printed'
|
||||||
|
else
|
||||||
|
fail 'control: the leak detector did not find a printed secret (vacuous check)'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6c. --fetch delivers the record next to the checkout at 0600/0700.
|
||||||
|
FETCH_REPO="$WORK/fetch-repo"
|
||||||
|
mkdir -p "$FETCH_REPO"
|
||||||
|
CREDS_FILE="$CSB/credentials.txt"
|
||||||
|
set +e
|
||||||
|
creds_env "$CREDS" "$WORK/creds-fetch.out" --fetch --repo "$FETCH_REPO"
|
||||||
|
fetch_rc=$?
|
||||||
|
set -e
|
||||||
|
FETCH_DEST="$FETCH_REPO/admin/secrets/12345678.txt"
|
||||||
|
expect_rc 'bf-creds --fetch exits 0' 0 "$fetch_rc"
|
||||||
|
expect_present 'bf-creds --fetch delivers the credential file' "$FETCH_DEST"
|
||||||
|
expect_mode 'the delivered file is mode 600' "$FETCH_DEST" 600
|
||||||
|
expect_mode 'the delivery directory is mode 700' "$FETCH_REPO/admin/secrets" 700
|
||||||
|
check
|
||||||
|
if cmp -s "$CSB/credentials.txt" "$FETCH_DEST"; then
|
||||||
|
pass 'the delivered file matches the credential record'
|
||||||
|
else
|
||||||
|
fail 'the delivered file differs from the credential record'
|
||||||
|
fi
|
||||||
|
expect_contains '--fetch reports only the delivery path' "$WORK/creds-fetch.out" "$FETCH_DEST"
|
||||||
|
expect_absent_text '--fetch never prints the generated password' "$WORK/creds-fetch.out" "$GENERATED_PASSWORD"
|
||||||
|
expect_absent_text '--fetch never prints the WireGuard private key' "$WORK/creds-fetch.out" "$FAKE_WG_PRIVATE"
|
||||||
|
|
||||||
|
# 6d. --fetch fails closed when the dealer number cannot be determined.
|
||||||
|
CREDS_FILE="$WORK/no-device/credentials.txt"
|
||||||
|
mkdir -p "$WORK/no-device"
|
||||||
|
cp "$CSB/credentials.txt" "$CREDS_FILE"
|
||||||
|
CREDS_STATE_FILE="$WORK/absent-state.json"
|
||||||
|
CREDS_DEVICE=''
|
||||||
|
set +e
|
||||||
|
creds_env "$CREDS" "$WORK/creds-nodevice.out" --fetch --repo "$FETCH_REPO"
|
||||||
|
nodevice_rc=$?
|
||||||
|
set -e
|
||||||
|
expect_rc 'bf-creds --fetch fails closed without a dealer number' 1 "$nodevice_rc"
|
||||||
|
expect_contains 'the failure explains the missing dealer number' "$WORK/creds-nodevice.out" 'cannot determine the dealer number'
|
||||||
|
CREDS_STATE_FILE="$CSB/state.json"
|
||||||
|
CREDS_DEVICE='BF-12345678'
|
||||||
|
|
||||||
|
# 6e. --dealer-id and --repo argument validation.
|
||||||
|
set +e
|
||||||
|
creds_env "$CREDS" "$WORK/creds-bad-dealer.out" --dealer-id 123 >/dev/null 2>&1
|
||||||
|
bad_dealer_rc=$?
|
||||||
|
creds_env "$CREDS" "$WORK/creds-bad-repo.out" --fetch --repo "$WORK/does-not-exist" >/dev/null 2>&1
|
||||||
|
bad_repo_rc=$?
|
||||||
|
set -e
|
||||||
|
expect_rc 'bf-creds rejects a malformed --dealer-id' 2 "$bad_dealer_rc"
|
||||||
|
expect_rc 'bf-creds rejects a --repo that does not exist' 2 "$bad_repo_rc"
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 7. .gitignore: the delivery path is ignored, the tools are not.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
check
|
||||||
|
if git -C "$REPO_ROOT" check-ignore -q -- 'admin/secrets/12345678.txt'; then
|
||||||
|
pass 'admin/secrets/<dealer-number>.txt is git-ignored'
|
||||||
|
else
|
||||||
|
fail 'the bf-creds delivery path is NOT git-ignored (add admin/secrets/ to .gitignore)'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Positive control for `git check-ignore` itself: a pattern that is definitely
|
||||||
|
# present must be reported as ignored, so the assertion above cannot pass by
|
||||||
|
# accident.
|
||||||
|
check
|
||||||
|
if git -C "$REPO_ROOT" check-ignore -q -- 'probe/example.key'; then
|
||||||
|
pass 'control: git check-ignore evaluates this repository'
|
||||||
|
else
|
||||||
|
fail 'control: git check-ignore did not report a *.key path as ignored'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The tools themselves must stay tracked: an over-broad rule would hide them.
|
||||||
|
check
|
||||||
|
if git -C "$REPO_ROOT" check-ignore -q -- 'admin/bf-bootstrap.sh'; then
|
||||||
|
fail 'admin/bf-bootstrap.sh is git-ignored'
|
||||||
|
else
|
||||||
|
pass 'admin/bf-bootstrap.sh is not git-ignored'
|
||||||
|
fi
|
||||||
|
check
|
||||||
|
if git -C "$REPO_ROOT" check-ignore -q -- 'admin/lib/creds.sh'; then
|
||||||
|
fail 'admin/lib/creds.sh is git-ignored'
|
||||||
|
else
|
||||||
|
pass 'admin/lib/creds.sh is not git-ignored'
|
||||||
|
fi
|
||||||
|
contract '.gitignore has no over-broad admin or catch-all rule' "$GITIGNORE" lacks '^([*]|admin/?)$'
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 8. The shipped admin files carry no secret-like material.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Heuristic for a *literal* credential assignment:
|
||||||
|
# * the separator is `:` or `=`,
|
||||||
|
# * the value is NOT a shell reference (`${...}`, `$(...)`),
|
||||||
|
# * the value contains a digit or a base64 symbol, which is what separates a
|
||||||
|
# credential literal from prose (a log message such as
|
||||||
|
# `bf_deliver_secret: destination required` must not be reported).
|
||||||
|
# A purely alphabetic secret would slip through: this is a guard against
|
||||||
|
# obviously planted material, not a replacement for the repository-wide scan.
|
||||||
|
SECRET_ASSIGNMENT='(^|[^A-Za-z])(password|passwd|token|api[_-]?key|secret)[[:space:]]*[:=][[:space:]]*[A-Za-z0-9+/=_.-]*[0-9+/=][A-Za-z0-9+/=_.-]*'
|
||||||
|
PRIVATE_KEY_BLOCK='BEGIN (OPENSSH|RSA|EC|DSA|PGP) PRIVATE KEY'
|
||||||
|
|
||||||
|
for target in "$BOOTSTRAP" "$CREDS" "$CREDS_LIB"; do
|
||||||
|
[[ -f "$target" ]] || continue
|
||||||
|
contract "no private key block in ${target#"$REPO_ROOT"/}" "$target" lacks_i "$PRIVATE_KEY_BLOCK"
|
||||||
|
contract "no secret assignment in ${target#"$REPO_ROOT"/}" "$target" lacks_i "$SECRET_ASSIGNMENT"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Negative controls: a planted secret must be found in each file.
|
||||||
|
for target in "$BOOTSTRAP" "$CREDS"; do
|
||||||
|
[[ -f "$target" ]] || continue
|
||||||
|
planted="$WORK/planted-$(basename "$target")"
|
||||||
|
cp "$target" "$planted"
|
||||||
|
printf '%s\n' 'DEMO_TOKEN=abcd1234efgh' >> "$planted"
|
||||||
|
control "secret assignment detection in $(basename "$target")" "$planted" lacks_i "$SECRET_ASSIGNMENT"
|
||||||
|
printf '%s\n' '-----BEGIN OPENSSH PRIVATE KEY-----' >> "$planted"
|
||||||
|
control "private key detection in $(basename "$target")" "$planted" lacks_i "$PRIVATE_KEY_BLOCK"
|
||||||
|
done
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Summary.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
if [[ "$FAIL" -ne 0 ]]; then
|
||||||
|
printf '\ncheck-admin-static: FAILED (%d checks)\n' "$CHECKS" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf '\ncheck-admin-static: all passed (%d checks)\n' "$CHECKS"
|
||||||
Loading…
Reference in a new issue