Blueforce-Linux/scripts/install/blueforce-install.sh
emirhanmamak-blueforce 503f4df2ae Enhance WireGuard restart playbook with operation wave validation
- Updated `bf-wireguard-restart.yml` to include dynamic host targeting based on `operation_wave`.
- Added pre-tasks to assert that the operation wave is approved and confirmed, ensuring proper execution context.
- Modified the service restart task to use a more structured command format.
- Adjusted `operations.py` and `ui_gui.py` to reflect changes in operation scope from "simple" to "wave" for relevant operations.
- Improved UI handling in `ui_gui.py` and `ui_surfaces.py` for better output management and user interaction.
- Updated log rotation configuration to target specific log files.
- Enhanced systemd service configurations for WireGuard to improve restart behavior.
- Refined firstboot scripts to ensure proper handling of dealer ID and state management.
- Improved error handling and rollback mechanisms in identity and installation scripts.
2026-09-17 17:06:58 +03:00

358 lines
19 KiB
Bash
Executable file
Raw Permalink 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)
trap 'rm -f "$STATE_FILE.tmp.$$"' EXIT
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" || { rm -f "$tmp"; return 1; }; }
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ış.'
# The pool is stored through Git LFS, so a checkout made with smudge disabled
# (bf-bootstrap does this deliberately: it avoids an 884 MB download) leaves
# 131-byte pointer texts where the .deb files should be. They satisfy `-f`
# above, so without this gate the run would die later inside `sha256sum -c`
# and report a checksum mismatch, which reads as a corrupt bundle instead of
# what it is: payload that was never fetched.
if [[ -e "$repo/pool" ]]; then
while IFS= read -r -d '' _pkgfile; do
if [[ "$(head -c 42 "$_pkgfile" 2>/dev/null)" == 'version https://git-lfs.github.com/spec/v1'* ]]; then
blocked_bundle "Paketler henüz indirilmemiş: depodaki dosyalar .deb değil, Git LFS işaretçisi ($(basename "$_pkgfile")). Çözüm: depo kökünde 'git lfs pull --include=\"provisioning/offline-repo/built/pool/*\"' çalıştırın, sonra bu kurulumu tekrarlayın."
fi
done < <(find "$repo/pool" -maxdepth 1 -type f -name '*.deb' -print0 2>/dev/null)
fi
(cd "$repo" && sha256sum -c SHA256SUMS >/dev/null) || blocked_bundle 'Yerel APT deposunun checksum doğrulaması başarısız.'
awk -F '\t' 'NR == 1 { if ($0 != "package\tversion\tarchitecture\tsha256\tsource") exit 1; next } $1 !~ /^[a-z0-9][a-z0-9+.-]*$/ || $2 == "" || $3 == "" || $4 !~ /^[a-f0-9]{64}$/ || $5 == "" { exit 1 } END { exit NR < 2 }' "$repo/packages.lock.tsv" || blocked_bundle 'Paket kilidi boş ya da biçimi bozuk.'
# Every locked row must be present in the pool: the lock file is the SBOM the
# 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:-}"; rm -f "$STATE_FILE.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.'
[[ -z "$FROM" || "$FROM" =~ ^[0-9]+$ ]] || usage_error '--from yalnız sayı olmalıdır (örn. --from 05).'
# Apply mode changes APT, services, firewall rules and /var/lib state. Fail
# before repository discovery so a desktop launch as a normal user produces a
# useful explanation instead of a later, opaque module exit code (usually from
# /var/log or the APT lock).
if [[ "$CHECK_MODE" -eq 0 && "$(id -u)" -ne 0 ]]; then
printf 'HATA: Kurulum yönetici yetkisi ister; bu komut normal kullanıcıyla çalıştırıldı.\n' >&2
printf ' Grafik uygulamayı yönetici yetkisiyle başlatın: sudo bf\n' >&2
printf ' Salt-okunur doğrulama için --check kullanılabilir.\n' >&2
exit 1
fi
# ------------------------------------------------------------------- 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:-}"
export BF_USER="bf-${DEALER_ID}" BF_IDENTITY_FILE="${BF_IDENTITY_FILE:-/etc/blueforce/dealer-id}"
if [[ "$CHECK_MODE" -eq 0 && "$ASSUME_YES" -eq 0 ]]; then
if ! read -r -p "Kurulum $BF_DEVICE olarak başlatılsın mı? [e/H] " answer; then
printf '\nKurulum iptal edildi: onay alınamadı (terminal yok). Otomatik kurulum için --yes ekleyin.\n' >&2
exit 2
fi
[[ "$answer" =~ ^[Ee]$ ]] || { printf 'Kurulum iptal edildi.\n'; exit 2; }
fi
RUN=(); for m in "${MODULES[@]}"; do
# Exact match only: a bare prefix ("1") must never also select 11-18. A pure
# number selects by numeric value (1 == 01-precheck, one module); anything
# else must name the full module entry.
if [[ -n "$ONLY" ]]; then
if [[ "$ONLY" =~ ^[0-9]+$ ]]; then [[ "10#${m%%-*}" -eq "10#$ONLY" ]] && RUN+=("$m")
else [[ "$m" == "$ONLY" ]] && RUN+=("$m"); fi
continue
fi
[[ -n "$FROM" && "10#${m%%-*}" -lt "10#$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
# Never step an ENROLLED/READY device back to PROVISIONED_OFFLINE: a rerun
# preserves the existing state instead of minting a fresh provisioning_id.
state_action="$(python3 - "$STATE_DIR/state.json" "$DEALER_ID" <<'PY'
import json, os, sys, tempfile, uuid
path, dealer = sys.argv[1:]
try:
with open(path, encoding='utf-8') as f:
existing = json.load(f)
except (OSError, ValueError):
existing = {}
phase = existing.get('phase')
if phase in ('ENROLLED', 'READY'):
print('PRESERVED ' + phase)
else:
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)
print('PROVISIONED')
PY
)"
if [[ "$state_action" == PRESERVED* ]]; then
log "state preserved (phase=${state_action#PRESERVED })"
else
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
fi
log "=== install done dealer=$BF_DEVICE failures=$failures ==="