Blueforce-Linux/scripts/install/blueforce-install.sh
emirhanmamak-blueforce 1387186b03 feat(bfos): Python operator console, Turkish UI, committable offline packages
Switches the operator path from bash-only to a Python console, keeps the bash
menu as a fallback, and makes the offline package set travel with the repository
so the person installing a device never downloads anything.

bfos/ (stdlib only, no textual/rich: those would need a download, which defeats
the offline goal):
- simple mode offers three choices (Kur / Durum / Çıkış); the detailed mode with
  all 21 fleet operations and the local installer is reachable from the menu or
  with --full, so a first-time operator is not buried in options
- gate parity with the bash console: scope read from the playbook file and
  refused on mismatch, production needs a typed token plus a separate approval,
  release-gated runs need their wave name typed, destructive runs need a second
  approval, "all" walks wave by wave and stops at the first failure
- Turkish throughout; audit log at /var/log/blueforce-console.log (600, falls
  back to the user state dir) with a single redaction choke point
- bfos/ui_gui.py adds an optional tkinter window over the same operations layer;
  headless and missing-tkinter cases explain themselves in Turkish

admin/bf-bootstrap.sh: links the Python console as `bf` (primary) and keeps
`bf-menu` (fallback), reports both, and recommends `sudo bf`.

Offline packages are committable now (this was the blocking contradiction):
- .gitignore ignored `Packages`, `Packages.gz` and `packages.lock.tsv` everywhere,
  so a built bundle could never be committed and the installer would always have
  been told to download. provisioning/offline-repo/built/ is now allowed through
  while scratch output stays untracked
- provisioning/offline-repo/{add-packages.sh,build-index.sh,repo-manifest.yaml}
  plus a Turkish step-by-step README for the repository owner
- the over-broad `*token*` ignore rule is narrowed to `*[-_.]token*` so ordinary
  filenames are not silently excluded while token files still are

Tests: run-all.sh now runs the Python suites too (previously it collected only
*.sh, so 212 Python assertions were silently skipped) and executes from the repo
root. 11/11 pass, exit 0.

Two test expectations were corrected rather than the code: a read-only
wave-scoped report legitimately asks for no gate token (the bash console never
did either), so that assertion now checks the command carries operation_wave and
operation_confirmed, and a new test asserts release-gated runs still require
their token. The bootstrap completion contract moved from `sudo bf-menu` to
`sudo bf`.
2026-09-16 16:15:27 +03:00

304 lines
16 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Blueforce one-click installer — single entry point for field setup.
#
# The field operator runs one command and answers no technical question:
#
# sudo ./scripts/install/blueforce-install.sh --dealer-id 12345678
#
# Packages always come from the local bundle the repository owner commits with
# the repository; nothing is downloaded at install time. The bundle is found
# automatically, in this order (the first complete candidate wins):
#
# 1. BF_OFFLINE_REPO operator override (must be valid),
# 2. <repo>/provisioning/offline-repo/built bundle committed to this repo,
# 3. /opt/blueforce/offline-repo bundle installed on the device.
#
# A missing or incomplete bundle stops the run before the first module, with a
# Turkish explanation, and changes nothing on the system (fail-closed).
#
# --offline is kept for backward compatibility (it used to be mandatory) but is
# no longer needed: it only forces the local bundle. --online exists for
# development/test hosts only and is never the default.
#
# User-facing messages are Turkish; code and comments are English.
# Exit codes: 0 success, 1 blocked or module failure, 2 usage error.
set -euo pipefail
INSTALLER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT_DIR="$(cd "$INSTALLER_DIR/../.." && pwd)"
MODULES_DIR="$INSTALLER_DIR/installer/modules"
MANIFEST_DIR="$REPO_ROOT_DIR/provisioning/offline-repo/manifests"
BUNDLE_IN_REPO="$REPO_ROOT_DIR/provisioning/offline-repo/built"
BUNDLE_ON_DEVICE="/opt/blueforce/offline-repo"
README_REF="provisioning/offline-repo/README.md"
LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}"
STATE_DIR="${STATE_DIR:-/var/lib/blueforce}"
STATE_FILE="$STATE_DIR/install-state"
MODULES=(01-precheck 02-system 03-user 04-hostname 05-network 06-wireguard 07-ssh 08-rdp 09-rustdesk 10-docker 11-meg 12-monitoring 13-firewall 14-update-policy 15-logrotate 16-gui 17-healthcheck 18-final-check)
DEALER_ID="" FROM="" ONLY="" CHECK_MODE=0 RESUME=0 ASSUME_YES=0 FORCE_OFFLINE=0 ONLINE=0
# The operator's override is remembered before any default is applied: an
# explicitly set path must fail closed instead of being replaced silently.
REPO_OVERRIDE="${BF_OFFLINE_REPO:-}"
REPO_DIR="" REPO_ORIGIN="" BF_OFFLINE=0
REPO_SEARCH=()
usage() {
cat <<'EOF'
Kullanım: sudo ./blueforce-install.sh --dealer-id <8 haneli numara> [seçenekler]
Paketler bu betiğin yanındaki yerel paket deposundan gelir. İnternet, paket
indirme ve teknik bilgi gerekmez; tek zorunlu bilgi bayi numarasıdır:
sudo ./blueforce-install.sh --dealer-id 12345678
Seçenekler:
--dealer-id NUMARA Zorunlu. 8 haneli bayi numarası (ana makine adı bf-NUMARA).
--yes, -y Onay sormadan başla (otomatik kurulum).
--check Salt okunur denetim: sistemde hiçbir şeyi değiştirmez.
--from NN NN numaralı modülden başla.
--only AD Yalnız belirtilen modülü çalıştır (örn. --only 05-network).
--resume Daha önce tamamlanmış modülleri atla.
--help, -h Bu yardımı yaz.
Yalnız geliştirme/test için (sahada gerekmez):
--offline Yerel depoyu kullanmayı zorla; varsayılan davranış zaten budur.
--online Paketleri internetten çeker. ÜRETİM cihazlarında KULLANMAYIN.
Paket deposu şu sırayla aranır: BF_OFFLINE_REPO ortam değişkeni, depodaki
provisioning/offline-repo/built dizini, /opt/blueforce/offline-repo.
Depo bulunamazsa kurulum başlamaz. Ayrıntı: scripts/install/README.md
EOF
}
log() {
if [[ "$CHECK_MODE" -eq 1 ]]; then
printf '[installer check] %s\n' "$1" >&2
else
mkdir -p "$(dirname "$LOG_FILE")" "$STATE_DIR"
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [installer] $1" | tee -a "$LOG_FILE"
fi
}
# notice: a message the operator must read. Always on stdout, and in apply mode
# also in the log file (the log stays the audit record of the run).
notice() {
printf '%s\n' "$1"
if [[ "$CHECK_MODE" -eq 0 ]]; then
mkdir -p "$(dirname "$LOG_FILE")"
printf '%s [installer] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$1" >> "$LOG_FILE"
fi
}
usage_error() { printf 'HATA: %s\n\n' "$1" >&2; usage >&2; exit 2; }
blocked_bundle() {
printf 'OFFLINE BLOCKED: %s\n' "$1" >&2
printf ' Depo : %s\n' "${REPO_DIR:-tanımsız}" >&2
printf ' Ne yapmalı : paket setini yeniden üretip git ile getirin (%s)\n' "$README_REF" >&2
printf ' ÇIKIŞ NEDENİ: yerel paket deposu doğrulanamadı; kurulum başlamadı ve\n' >&2
printf ' sistemde hiçbir değişiklik yapılmadı.\n' >&2
exit 1
}
record_state() { mkdir -p "$STATE_DIR"; local tmp="$STATE_FILE.tmp"; grep -v "^$1=" "$STATE_FILE" 2>/dev/null > "$tmp" || true; echo "$1=$2" >> "$tmp"; mv "$tmp" "$STATE_FILE"; }
state_of() { grep "^$1=" "$STATE_FILE" 2>/dev/null | cut -d= -f2 || true; }
# ------------------------------------------------------- bundle discovery -----
human_size() {
awk -v b="$1" 'BEGIN {
split("B KiB MiB GiB TiB", unit, " ")
i = 1
while (b >= 1024 && i < 5) { b /= 1024; i++ }
if (i == 1) printf "%d %s\n", b, unit[i]; else printf "%.1f %s\n", b, unit[i]
}'
}
repository_bytes() {
local dir="$1" bytes
bytes="$(du -sb "$dir" 2>/dev/null | awk '{print $1}')"
if [[ ! "$bytes" =~ ^[0-9]+$ ]]; then
bytes="$(du -sk "$dir" 2>/dev/null | awk '{print $1 * 1024}')"
fi
[[ "$bytes" =~ ^[0-9]+$ ]] || bytes=0
printf '%s' "$bytes"
}
repository_package_count() {
awk -F '\t' 'NR > 1 && NF >= 5 { n++ } END { printf "%d", n + 0 }' "$1/packages.lock.tsv"
}
repository_is_complete() {
local repo="$1"
[[ -n "$repo" && -d "$repo" && -s "$repo/Packages" && -s "$repo/Packages.gz" && -s "$repo/packages.lock.tsv" && -s "$repo/SHA256SUMS" ]]
}
# Fills REPO_DIR/REPO_ORIGIN and REPO_SEARCH (one readable line per candidate).
discover_offline_repository() {
local -a dirs=() labels=()
local i dir label
if [[ -n "$REPO_OVERRIDE" ]]; then
dirs=("$REPO_OVERRIDE"); labels=("BF_OFFLINE_REPO ortam değişkeni")
else
dirs=("$BUNDLE_IN_REPO" "$BUNDLE_ON_DEVICE")
labels=("depodaki hazır paket seti" "cihazdaki kurulu paket deposu")
fi
for i in "${!dirs[@]}"; do
dir="${dirs[$i]}"; label="${labels[$i]}"
if [[ ! -e "$dir" ]]; then
REPO_SEARCH+=("$label — yok: $dir")
continue
fi
if repository_is_complete "$dir"; then
REPO_DIR="$dir"; REPO_ORIGIN="$label"
REPO_SEARCH+=("$label — kullanılabilir: $dir")
return 0
fi
REPO_SEARCH+=("$label — eksik/bozuk: $dir (Packages, Packages.gz, packages.lock.tsv ve SHA256SUMS dosyalarının hepsi gerekli)")
done
return 1
}
report_missing_repository() {
local entry
printf 'HATA: Yerel paket deposu yok. Repo sorumlusuna başvurun: %s\n' "$README_REF" >&2
printf '\n' >&2
printf 'Kurulum paketleri yalnız yerel depodan kurar; bu makinede internetten paket\n' >&2
printf 'alınmaz. Depo olmadığı için paket adımları çalışamaz.\n' >&2
printf '\nAranan yerler:\n' >&2
for entry in "${REPO_SEARCH[@]}"; do
printf ' - %s\n' "$entry" >&2
done
printf '\nNe yapmalı:\n' >&2
printf ' 1) Paket setini git ile tam olarak getirin (repo sorumlusunun hazırladığı\n' >&2
printf ' provisioning/offline-repo/built dizini) ya da repo sorumlusunun verdiği\n' >&2
printf ' kopyayı /opt/blueforce/offline-repo altına koyun.\n' >&2
printf ' 2) Kurulumu yeniden çalıştırın: sudo ./blueforce-install.sh --dealer-id <numara>\n' >&2
printf '\nKuruluma hiç başlanmadı; sistemde hiçbir değişiklik yapılmadı.\n' >&2
printf 'ÇIKIŞ NEDENİ: yerel paket deposu bulunamadı.\n' >&2
exit 1
}
# ------------------------------------------------------ bundle validation -----
# Fail-closed: every check has to pass before the first module runs. The local
# APT source and the pinned versions are the only package contract of the
# install, so a bundle that does not match its own lock file is refused.
validate_offline_repository() {
local repo="$REPO_DIR" manifest pin package version source lpackage lversion larch lsha lsource
[[ "$repo" = /* && -d "$repo" ]] || blocked_bundle 'BF_OFFLINE_REPO mutlak bir yerel dizin olmalıdır.'
repository_is_complete "$repo" || blocked_bundle 'Yerel APT indeksi, kilit dosyası ya da checksum kaydı eksik; paket seti tamamlanmamış.'
(cd "$repo" && sha256sum -c SHA256SUMS >/dev/null) || blocked_bundle 'Yerel APT deposunun checksum doğrulaması başarısız.'
awk -F '\t' 'NR == 1 { if ($0 != "package\tversion\tarchitecture\tsha256\tsource") exit 1; next } $1 !~ /^[a-z0-9][a-z0-9+.-]*$/ || $2 == "" || $3 == "" || $4 !~ /^[a-f0-9]{64}$/ || $5 == "" { exit 1 } END { exit NR < 2 }' "$repo/packages.lock.tsv" || blocked_bundle 'Paket kilidi boş ya da biçimi bozuk.'
# Every locked row must be present in the pool: the lock file is the SBOM the
# bundle is installed from, so a row without its file is an incomplete set.
while IFS=$'\t' read -r lpackage lversion larch lsha lsource; do
[[ "$lpackage" == 'package' && "$lversion" == 'version' ]] && continue
[[ -n "$lsource" && -f "$repo/pool/$lsource" ]] || blocked_bundle "Kilitte yazılı paket depoda yok: $lpackage $lversion ($lsource)."
done < "$repo/packages.lock.tsv"
# The pin manifests are the reviewed "what must be installed" contract of the
# repository; when they travel with the checkout they are enforced too.
if [[ -d "$MANIFEST_DIR" ]]; then
for manifest in "$MANIFEST_DIR"/*.txt; do
[[ -e "$manifest" ]] || continue
[[ -s "$manifest" ]] || blocked_bundle "Gerekli pin manifestosu boş: $(basename "$manifest")."
grep -qvE '^[[:space:]]*(#|$)' "$manifest" || blocked_bundle "Gerekli pin manifestosunda paket pini yok: $(basename "$manifest")."
while IFS= read -r pin || [[ -n "$pin" ]]; do
[[ -z "$pin" || "$pin" == \#* ]] && continue
[[ "$pin" =~ ^[a-z0-9][a-z0-9+.-]*=[^[:space:]]+$ ]] && [[ "$pin" != *placeholder* && "$pin" != *REPLACE* && "$pin" != *replace* ]] || blocked_bundle "Geçersiz paket pini: $(basename "$manifest") içinde '$pin'."
package="${pin%%=*}"; version="${pin#*=}"
source="$(awk -F '\t' -v p="$package" -v v="$version" '$1 == p && $2 == v {print $5; exit}' "$repo/packages.lock.tsv")"
[[ -n "$source" && -f "$repo/pool/$source" ]] || blocked_bundle "Pinlenen paket depoda yok: $package=$version."
done < "$manifest"
done
else
log "BİLGİ: pin manifestoları bu makinede yok; sözleşme packages.lock.tsv üzerinden doğrulandı."
fi
# The local APT source. --check stays read-only: its apt configuration lives
# in a temporary directory and is removed on exit.
if [[ "$CHECK_MODE" -eq 1 ]]; then
CHECK_TMP="$(mktemp -d)"
trap 'rm -rf "${CHECK_TMP:-}"' EXIT
printf 'deb [trusted=yes] file:%s ./\n' "$repo" > "$CHECK_TMP/offline.list"
BF_OFFLINE_APT_CONFIG="$CHECK_TMP/apt.conf"
printf 'Dir::Etc::sourcelist "%s";\nDir::Etc::sourceparts "-";\nAcquire::Languages "none";\n' "$CHECK_TMP/offline.list" > "$BF_OFFLINE_APT_CONFIG"
chmod 600 "$CHECK_TMP/offline.list" "$BF_OFFLINE_APT_CONFIG"
else
install -d -m 700 "$STATE_DIR"
printf 'deb [trusted=yes] file:%s ./\n' "$repo" > "$STATE_DIR/offline.list"
chmod 600 "$STATE_DIR/offline.list"
BF_OFFLINE_APT_CONFIG="$(mktemp)"; chmod 600 "$BF_OFFLINE_APT_CONFIG"
printf 'Dir::Etc::sourcelist "%s";\nDir::Etc::sourceparts "-";\nAcquire::Languages "none";\n' "$STATE_DIR/offline.list" > "$BF_OFFLINE_APT_CONFIG"
fi
export BF_OFFLINE_APT_CONFIG
}
# --------------------------------------------------------------- arguments ----
while [[ $# -gt 0 ]]; do case "$1" in
--dealer-id) DEALER_ID="${2:-}"; shift 2;;
--offline) FORCE_OFFLINE=1; shift;;
--online) ONLINE=1; shift;;
--from) FROM="${2:-}"; shift 2;;
--only) ONLY="${2:-}"; shift 2;;
--check) CHECK_MODE=1; shift;;
--resume) RESUME=1; shift;;
--yes|-y) ASSUME_YES=1; shift;;
--help|-h) usage; exit 0;;
*) usage_error "Bilinmeyen parametre: $1";;
esac; done
[[ -n "$DEALER_ID" ]] || usage_error 'Zorunlu parametre eksik: --dealer-id <8 haneli bayi numarası>. Örnek: sudo ./blueforce-install.sh --dealer-id 12345678'
[[ "$DEALER_ID" =~ ^[0-9]{8}$ ]] || usage_error "Geçersiz bayi numarası '$DEALER_ID': 8 haneli rakam olmalıdır (örn. 12345678)."
[[ "$FORCE_OFFLINE" -eq 1 && "$ONLINE" -eq 1 ]] && usage_error 'Bir kurulumda hem --offline hem --online verilemez.'
# ------------------------------------------------------------------- mode -----
# Default (no flag): find the local bundle and install from it. The flag the
# operator used to have to remember is now only a way to insist on it.
if [[ "$ONLINE" -eq 1 ]]; then
BF_OFFLINE=0
notice 'DİKKAT: --online yalnız geliştirme/test içindir. Paketler uzak APT kaynaklarından'
notice ' çekilir; ÜRETİM cihazlarında KULLANMAYIN.'
export BF_OFFLINE_REPO=""
else
if ! discover_offline_repository; then
report_missing_repository
fi
BF_OFFLINE=1
notice "Yerel paket deposu bulundu: $(repository_package_count "$REPO_DIR") paket, $(human_size "$(repository_bytes "$REPO_DIR")")"
notice " Depo : $REPO_DIR"
notice " Kaynak : $REPO_ORIGIN"
notice ' Kip : yerel kurulum. İnternet, paket indirme ve ek anahtar gerekmez.'
if [[ "$FORCE_OFFLINE" -eq 1 ]]; then
log 'BİLGİ: --offline verildi; yerel depo zaten varsayılan kaynaktır.'
fi
validate_offline_repository
fi
export DEALER_ID BF_HOSTNAME="bf-${DEALER_ID}" BF_DEVICE="BF-${DEALER_ID}" STATE_DIR BF_OFFLINE BF_OFFLINE_REPO="${REPO_DIR:-}"
if [[ "$CHECK_MODE" -eq 0 && "$ASSUME_YES" -eq 0 ]]; then
read -r -p "Kurulum $BF_DEVICE olarak başlatılsın mı? [e/H] " answer
[[ "$answer" =~ ^[Ee]$ ]] || { echo 'Kurulum iptal edildi.'; exit 2; }
fi
RUN=(); for m in "${MODULES[@]}"; do
if [[ -n "$ONLY" ]]; then [[ "$m" == "$ONLY" || "$m" == "$ONLY"* ]] && RUN+=("$m"); continue; fi
[[ -n "$FROM" && "${m%%-*}" < "$FROM" ]] && continue
if [[ "$CHECK_MODE" -eq 0 && "$RESUME" -eq 1 && "$(state_of "$m")" == OK ]]; then log "SKIP $m (already OK)"; continue; fi
RUN+=("$m")
done
[[ ${#RUN[@]} -gt 0 ]] || { echo "No modules selected." >&2; exit 2; }
log "=== install start dealer=$BF_DEVICE mode=$([[ "$CHECK_MODE" -eq 1 ]] && echo check || echo apply) source=$([[ "$BF_OFFLINE" -eq 1 ]] && echo local || echo remote) modules=${RUN[*]} ==="
failures=0
for m in "${RUN[@]}"; do
script="$MODULES_DIR/$m.sh"; if [[ ! -x "$script" ]]; then log "FAIL $m (script missing)"; [[ "$CHECK_MODE" -eq 0 ]] && record_state "$m" FAIL; failures=$((failures+1)); break; fi
args=(); [[ "$CHECK_MODE" -eq 1 ]] && args+=(--check)
set +e; if [[ "$CHECK_MODE" -eq 1 ]]; then LOG_FILE=/dev/null bash "$script" "${args[@]}"; else LOG_FILE="$LOG_FILE" bash "$script"; fi; rc=$?; set -e
case "$rc" in 0) log "OK $m"; [[ "$CHECK_MODE" -eq 0 ]] && record_state "$m" OK;; 3) log "SKIP $m"; [[ "$CHECK_MODE" -eq 0 ]] && record_state "$m" SKIP;; *) log "FAIL $m (exit $rc)"; [[ "$CHECK_MODE" -eq 0 ]] && record_state "$m" FAIL; failures=$((failures+1)); [[ "$CHECK_MODE" -eq 0 ]] && exit 1;; esac
done
[[ "$CHECK_MODE" -eq 1 && "$failures" -gt 0 ]] && exit 1
if [[ "$CHECK_MODE" -eq 0 && "$BF_OFFLINE" -eq 1 && "$failures" -eq 0 ]]; then
install -d -m 700 "$STATE_DIR"
umask 077
python3 - "$STATE_DIR/state.json" "$DEALER_ID" <<'PY'
import json, os, sys, tempfile, uuid
path, dealer = sys.argv[1:]
state={'phase':'PROVISIONED_OFFLINE','enrollment_status':'PENDING','fleet_status':'PENDING','device_id':'BF-'+dealer,'provisioning_id':str(uuid.uuid4())}
fd,tmp=tempfile.mkstemp(prefix='state.json.',dir=os.path.dirname(path)); os.fchmod(fd,0o600)
with os.fdopen(fd,'w') as f: json.dump(state,f,sort_keys=True); f.write('\n')
os.replace(tmp,path)
PY
chmod 600 "$STATE_DIR/state.json"
log "OFFLINE COMPLETE: device is PROVISIONED_OFFLINE; enrollment is required before READY"
notice "Kurulum tamam: $BF_DEVICE yerel paket deposundan kuruldu."
notice 'Cihaz şu an PROVISIONED_OFFLINE durumunda; kayıt (enrollment) tamamlanana kadar READY sayılmaz.'
fi
log "=== install done dealer=$BF_DEVICE failures=$failures ==="