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`.
304 lines
16 KiB
Bash
Executable file
304 lines
16 KiB
Bash
Executable file
#!/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 ==="
|