Blueforce-Linux/scripts/install/blueforce-install.sh
emirhanmamak-blueforce ef102b9657 feat(field-os): provisioning layer, real ISO build, offline contracts
Adds the Blueforce Field OS provisioning stack on top of the existing fleet
architecture, without rewriting the working systems (installer, diagnostics,
Ansible, WireGuard/SSH/xRDP/RustDesk, Docker policies, monitoring, Docusaurus).

ISO build (the previous builder was a skeleton that produced no medium):
- boot layout is read from the base ISO at build time via
  `xorriso -report_el_torito as_mkisofs` and rebuilt with `-as mkisofs`;
  `-boot_image any replay` cannot work on Ubuntu live ISOs because the GRUB2
  MBR lives in the source image's first 16 sectors and the EFI system partition
  is an appended partition, neither of which is a file in the ISO9660 tree
- preflight now asserts El Torito boot metadata positively: xorriso exits 0 on
  a non-ISO input, which previously let an arbitrary file pass --check
- artifact produced and verified: dist/Blueforce-Field-OS-1.0.0-amd64.iso,
  6 481 917 952 bytes, SHA256 cb0cc56f...d431296, BIOS+UEFI entries preserved,
  /autoinstall.yaml at the media root, casper/minimal.squashfs byte-identical
  to the base
- recorded baseline deviation: the verified medium on the build host is the
  Desktop flavour, so the manifest carries a baseline_deviation block instead
  of hiding it; Server remains one flag away

Offline and identity:
- offline APT contract with concrete package requirements, pin format and an
  audit table; builds fail closed while pins are UNPINNED
- autoinstall keeps interactive-sections: [storage]: the operator confirms the
  target disk, and no automatic wipe is ever performed
- firstboot terminal wizard for the 8-digit dealer id, three-state lifecycle
  (PROVISIONED_OFFLINE -> ENROLLED -> READY) with fail-closed gates, so a
  missing internet link or WireGuard handshake cannot fail provisioning but can
  never produce a false READY

Fleet and migration:
- five read-only Ansible playbooks for Field OS version, provisioning,
  enrollment, remote channels and offline readiness, all wave-scoped with a
  separate production override
- Windows pre-migration inventory and data export extended to the full brief
  scope, read-only, never collecting credentials

Tests: tests/run-all.sh -> 8/8 pass, exit 0. Boot acceptance (UEFI, Legacy
BIOS, Secure Boot), air-gapped install, real enrollment endpoint and Windows
rollback remain LAB work and are listed in IMPLEMENTATION-REPORT.md.
2026-09-16 11:14:42 +03:00

83 lines
No EOL
6.9 KiB
Bash
Executable file

#!/usr/bin/env bash
# Blueforce one-click installer — single entry point for field setup.
set -euo pipefail
INSTALLER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MODULES_DIR="$INSTALLER_DIR/installer/modules"
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 OFFLINE=0
BF_OFFLINE_REPO="${BF_OFFLINE_REPO:-/opt/blueforce/offline-repo}"
usage() { cat <<'EOF'
Usage: sudo ./blueforce-install.sh --dealer-id <8 digits> [--offline] [--from NN] [--only NAME] [--check] [--resume] [--yes]
--offline uses supplied local packages only and completes as PROVISIONED_OFFLINE.
--check is read-only: it creates no logs, state, or directories.
EOF
}
while [[ $# -gt 0 ]]; do case "$1" in --dealer-id) DEALER_ID="${2:-}"; shift 2;; --offline) OFFLINE=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;; *) echo "Unknown argument: $1" >&2; usage >&2; exit 2;; esac; done
[[ -n "$DEALER_ID" ]] || { echo "Missing --dealer-id (8 digits)" >&2; exit 2; }
[[ "$DEALER_ID" =~ ^[0-9]{8}$ ]] || { echo "Invalid dealer id '$DEALER_ID': must match ^[0-9]{8}$" >&2; exit 2; }
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; }
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; }
export DEALER_ID BF_HOSTNAME="bf-${DEALER_ID}" BF_DEVICE="BF-${DEALER_ID}" STATE_DIR BF_OFFLINE="$OFFLINE" BF_OFFLINE_REPO
validate_offline_repository() {
local manifest pin package version source
[[ "$BF_OFFLINE_REPO" = /* && -d "$BF_OFFLINE_REPO" ]] || { echo "OFFLINE BLOCKED: BF_OFFLINE_REPO must be an existing absolute local directory." >&2; return 1; }
[[ -s "$BF_OFFLINE_REPO/Packages" && -s "$BF_OFFLINE_REPO/Packages.gz" && -s "$BF_OFFLINE_REPO/packages.lock.tsv" && -s "$BF_OFFLINE_REPO/SHA256SUMS" ]] || { echo 'OFFLINE BLOCKED: local APT index, lock, or checksums are missing; ISO bundle is incomplete.' >&2; return 1; }
(cd "$BF_OFFLINE_REPO" && sha256sum -c SHA256SUMS >/dev/null) || { echo 'OFFLINE BLOCKED: local APT repository checksum validation failed.' >&2; return 1; }
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 }' "$BF_OFFLINE_REPO/packages.lock.tsv" || { echo 'OFFLINE BLOCKED: package lock is empty or invalid.' >&2; return 1; }
for manifest in "$INSTALLER_DIR/../../provisioning/offline-repo/manifests"/*.txt; do
[[ -s "$manifest" ]] || { echo "OFFLINE BLOCKED: required pin manifest is empty: $(basename "$manifest")." >&2; return 1; }
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* ]] || { echo "OFFLINE BLOCKED: invalid package pin in $(basename "$manifest")." >&2; return 1; }
package="${pin%%=*}"; version="${pin#*=}"
source="$(awk -F '\t' -v p="$package" -v v="$version" '$1 == p && $2 == v {print $5; exit}' "$BF_OFFLINE_REPO/packages.lock.tsv")"
[[ -n "$source" && -f "$BF_OFFLINE_REPO/pool/$source" ]] || { echo "OFFLINE BLOCKED: pinned package absent from bundle: $package." >&2; return 1; }
done < "$manifest"
grep -qvE '^[[:space:]]*(#|$)' "$manifest" || { echo "OFFLINE BLOCKED: required pin manifest has no package pins: $(basename "$manifest")." >&2; return 1; }
done
install -d -m 700 "$STATE_DIR"
printf 'deb [trusted=yes] file:%s ./\n' "$BF_OFFLINE_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"
export BF_OFFLINE_APT_CONFIG
}
if [[ "$OFFLINE" -eq 1 ]]; then
validate_offline_repository || exit 1
fi
if [[ "$CHECK_MODE" -eq 0 && "$ASSUME_YES" -eq 0 ]]; then read -r -p "Proceed with installation as $BF_DEVICE? [y/N] " answer; [[ "$answer" =~ ^[Yy]$ ]] || { echo Aborted.; 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) 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 && "$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"
fi
log "=== install done dealer=$BF_DEVICE failures=$failures ==="