#!/bin/bash # Revision: 4 # First-boot provisioning for reTerminal DM — two-phase process. # Phase 1 (cloud-init): hostname, packages, reTerminal DM drivers → reboot # Phase 2 (systemd oneshot): kiosk, theme, LightDM, rotation → reboot # # Step implementations are downloaded from FILE_SERVER/steps/ and sourced. # All fileserver assets live in cloud-init/fileserver/ in the repo. # # REQUIRES first-boot.conf — looked up in order: # 1. /first-boot.conf # 2. /tmp/first-boot.conf (cloud-init runcmd downloads it) # 3. /etc/cm4-provisioning/first-boot.conf # 4. /var/lib/cm4-provisioning/first-boot.conf (persisted for phase 2) # Copy first-boot.conf.example → first-boot.conf and fill in all values. # Disable steps: ENABLE_STEP_01=0 .. ENABLE_STEP_13=0 set -e export DEBIAN_FRONTEND=noninteractive # ── Phase tracking ────────────────────────────────────────────────────── STATE_DIR="/var/lib/cm4-provisioning" PHASE_FILE="$STATE_DIR/first-boot-phase" PERSISTENT_SCRIPT="$STATE_DIR/first-boot.sh" PERSISTENT_CONF="$STATE_DIR/first-boot.conf" PHASE2_SERVICE="cm4-first-boot-phase2" STEPS_DIR="/tmp/first-boot-steps" LOGFILE="/var/log/first-boot.log" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")" 2>/dev/null && pwd)" # ── Load config (required) ────────────────────────────────────────────── FIRST_BOOT_CONF="" for _f in "$SCRIPT_DIR/first-boot.conf" /tmp/first-boot.conf \ /etc/cm4-provisioning/first-boot.conf "$PERSISTENT_CONF"; do if [[ -f "$_f" ]]; then set -a && source "$_f" && set +a FIRST_BOOT_CONF="$_f" break fi done if [[ -z "$FIRST_BOOT_CONF" ]]; then echo "[$(date -Iseconds)] ERROR: first-boot.conf not found. Searched:" >&2 echo " $SCRIPT_DIR/first-boot.conf" >&2 echo " /tmp/first-boot.conf" >&2 echo " /etc/cm4-provisioning/first-boot.conf" >&2 echo " $PERSISTENT_CONF" >&2 exit 1 fi # Step enable flags (default: all enabled) for _n in 01 02 03 04 05 06 07 08 09 10 11 12 13 14; do eval "ENABLE_STEP_${_n}=\"\${ENABLE_STEP_${_n}:-1}\"" done # ── Derived ───────────────────────────────────────────────────────────── PI_HOME="/home/$PI_USER" AUTOSTART="$PI_HOME/.config/autostart" [[ "$FILE_SERVER" =~ ^(https?://[^/]+) ]] && PORTAL_BASE="${BASH_REMATCH[1]}" || PORTAL_BASE="" [[ -f "$PHASE_FILE" ]] && CURRENT_PHASE="$(cat "$PHASE_FILE")" || CURRENT_PHASE="1" # ── Logging ───────────────────────────────────────────────────────────── log() { echo "[$(date -Iseconds)] $*"; } exec > >(tee -a "$LOGFILE") 2>&1 log "=== first-boot.sh phase $CURRENT_PHASE ===" log "Config: $FIRST_BOOT_CONF" log "FILE_SERVER=$FILE_SERVER PI_USER=$PI_USER HOSTNAME=$HOSTNAME" # ── Portal status API ────────────────────────────────────────────────── report_status() { [[ -z "$PORTAL_BASE" ]] && return 0 curl -s --connect-timeout 5 --max-time 10 -X POST -H "Content-Type: application/json" \ -d "{\"phase\":\"$1\",\"message\":\"$2\",\"step\":\"$3\",\"step_name\":\"$4\",\"hostname\":\"$HOSTNAME\",\"ip\":\"$5\"}" \ "${PORTAL_BASE}/api/first-boot-status" || true } report_status "started" "Phase $CURRENT_PHASE started" "" "" "" # ── Helpers (available to step scripts) ───────────────────────────────── install_oneshot() { local name="$1" curl -fsSL "${FILE_SERVER}/${name}.sh" -o "$PI_HOME/${name}.sh" || { log "WARNING: ${name}.sh download failed"; return 1; } curl -fsSL "${FILE_SERVER}/${name}.desktop" -o "$AUTOSTART/${name}.desktop" || { log "WARNING: ${name}.desktop download failed"; return 1; } chmod 755 "$PI_HOME/${name}.sh" chmod 644 "$AUTOSTART/${name}.desktop" chown "$PI_USER:$PI_USER" "$PI_HOME/${name}.sh" "$AUTOSTART/${name}.desktop" log "One-shot $name installed" } # ── Download, source, and run a step ──────────────────────────────────── run_step() { local n="$1" name="$2" local val; eval "val=\"\${ENABLE_STEP_${n}}\"" val="${val//[^01]/}" if [[ "${val:0:1}" != "1" ]]; then log "--- Step $n: $name (disabled) ---" return 0 fi log "--- Step $n: $name ---" local f="$STEPS_DIR/${n}-${name}.sh" curl -fsSL "${FILE_SERVER}/steps/${n}-${name}.sh" -o "$f" \ || { log "ERROR: could not download step ${n}-${name}.sh"; return 1; } source "$f" "step_${n}_${name}" report_status "running" "Step $n: $name done" "$n" "$name" "" } # ── Phase transitions ────────────────────────────────────────────────── phase1_reboot() { mkdir -p "$STATE_DIR" cp "$(readlink -f "${BASH_SOURCE[0]:-.}")" "$PERSISTENT_SCRIPT" chmod 755 "$PERSISTENT_SCRIPT" [[ -n "$FIRST_BOOT_CONF" && -f "$FIRST_BOOT_CONF" && "$FIRST_BOOT_CONF" != "$PERSISTENT_CONF" ]] \ && cp "$FIRST_BOOT_CONF" "$PERSISTENT_CONF" echo "2" > "$PHASE_FILE" cat > "/etc/systemd/system/${PHASE2_SERVICE}.service" </dev/null || true rm -f "/etc/systemd/system/${PHASE2_SERVICE}.service" systemctl daemon-reload rm -f "$PHASE_FILE" "$PERSISTENT_SCRIPT" "$PERSISTENT_CONF" log "Phase 2 cleanup done" } # ── Main ──────────────────────────────────────────────────────────────── mkdir -p "$STEPS_DIR" if [[ "$CURRENT_PHASE" == "1" ]]; then log "=== PHASE 1: hostname, packages, reTerminal drivers ===" run_step 01 hostname run_step 02 packages run_step 03 reterminal_drivers phase1_reboot elif [[ "$CURRENT_PHASE" == "2" ]]; then log "=== PHASE 2: kiosk, theme, display config ===" run_step 04 kiosk_files run_step 05 splash_wallpaper run_step 06 lightdm run_step 07 maliit run_step 08 dark_theme run_step 09 reapply_splash run_step 10 cmdline run_step 11 oneshots run_step 12 log_permissions run_step 14 screen_brightness phase2_cleanup run_step 13 reboot # Only reached if step 13 (reboot) is disabled DEVICE_IP="$(hostname -I 2>/dev/null | awk '{print $1}')" report_status "done" "First-boot complete (reboot disabled)" "13" "reboot" "$DEVICE_IP" log "=== first-boot.sh finished (reboot disabled) ===" else log "ERROR: Unknown phase '$CURRENT_PHASE'"; exit 1 fi