#!/usr/bin/env bash # investigate-boot - Systematic IncusOS boot failure investigation # # Runs repeated deploy/destroy cycles with observe-deploy-style screenshot # capture, PLUS probes the IncusOS REST API after each deploy to capture # state.txt contents. Records structured results for analysis. # # Supports different ISO sources to test the version mismatch hypothesis: # --iso-source cdn Use latest CDN ISO (downloads if not on Proxmox) # --iso-source old Use IncusOS_202602200553.iso (already on Proxmox) # --iso-source FILE Use a specific ISO file path # # Usage: # ./investigate-boot --runs 5 --iso-source old --label old-iso # ./investigate-boot --runs 5 --iso-source cdn --label new-iso # ./investigate-boot --runs 5 --iso-source /tmp/IncusOS-oc.iso --label oc-iso # ./investigate-boot --dry-run set -euo pipefail readonly VERSION="0.1.0" readonly SCRIPT_NAME="investigate-boot" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SEED_TOOL="${SCRIPT_DIR}/incusos-seed" # --------------------------------------------------------------------------- # Defaults # --------------------------------------------------------------------------- VMID=850 VM_NAME="investigate-boot" RUNS=5 ISO_SOURCE="old" # "cdn", "old", or a file path LABEL="" DRY_RUN=false SCREENSHOT_INTERVAL=2 INSTALL_TIMEOUT=600 BOOT_TIMEOUT=180 INSTALL_POLL=5 STABLE_THRESHOLD=3 APPLY_DEFAULTS=true # true for standalone, false for OC-destined nodes # Proxmox connection (from proxmox.yaml) PVE_HOST="" PVE_NODE="" PVE_STORAGE="" PVE_ISO_STORAGE="" PVE_BRIDGE="" PVE_POOL="" PVE_API_TOKEN_ID="" # ISO tracking ISO_FILENAME="" ISO_VERSION="" ISO_UPLOADED_BY_US=false # Run output RESULTS_DIR="" RUN_DIR="" LOG_FILE="" FRAME_NUM=0 # State tracking PHASE="init" PREV_WR_BYTES=0 WRITES_STARTED=false STABLE_COUNT=0 # Results accumulator declare -a RUN_OUTCOMES=() declare -a RUN_TIMES=() declare -a RUN_SCRUB_SCHEDULES=() declare -a RUN_CHECK_FREQUENCIES=() declare -a RUN_OS_VERSIONS=() declare -a RUN_DIRS=() # --------------------------------------------------------------------------- # Colors # --------------------------------------------------------------------------- setup_colors() { if [[ -t 1 ]] && [[ "${NO_COLOR:-}" != "1" ]] && [[ "${TERM:-}" != "dumb" ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m' else RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' RESET='' fi } setup_colors step() { echo -e "${CYAN}[step]${RESET} ${BOLD}$*${RESET}"; } success() { echo -e "${GREEN}[ok]${RESET} $*"; } warn() { echo -e "${YELLOW}[warn]${RESET} $*" >&2; } error() { echo -e "${RED}[error]${RESET} $*" >&2; } info() { echo -e "${BLUE}[info]${RESET} $*"; } detail() { echo -e "${DIM} $*${RESET}"; } log() { local ts ts=$(date '+%H:%M:%S.%3N') local msg="[${ts}] $*" echo -e "${DIM}${msg}${RESET}" [[ -n "${LOG_FILE:-}" ]] && echo "$msg" >> "$LOG_FILE" } log_quiet() { local ts ts=$(date '+%H:%M:%S.%3N') [[ -n "${LOG_FILE:-}" ]] && echo "[${ts}] $*" >> "$LOG_FILE" } # --------------------------------------------------------------------------- # Argument parsing # --------------------------------------------------------------------------- while [[ $# -gt 0 ]]; do case "$1" in --vmid) VMID="${2:?'--vmid requires a number'}"; shift 2 ;; --runs) RUNS="${2:?'--runs requires a number'}"; shift 2 ;; --iso-source) ISO_SOURCE="${2:?'--iso-source requires cdn|old|path'}"; shift 2 ;; --label) LABEL="${2:?'--label requires a string'}"; shift 2 ;; --no-defaults) APPLY_DEFAULTS=false; shift ;; --interval) SCREENSHOT_INTERVAL="${2:?'--interval requires seconds'}"; shift 2 ;; --dry-run) DRY_RUN=true; shift ;; -h|--help) cat <_