homelab-codex-ws/scripts/onboard/steps/00-preflight.sh
Oskar Kapala adb84079ab feat(onboard): add node onboarding scaffold (bash, idempotent)
- scripts/onboard/onboard.sh: orchestrator with --node/--step/--from/--dry-run flags,
  deploy_autonomy + git_control gates, lexicographic step ordering
- scripts/onboard/lib/common.sh: log/warn/die/step helpers, yaml_get (yq+grep/sed fallback),
  ensure_line, git() wrapper enforcing --no-pager
- scripts/onboard/lib/remote.sh: rrun/rcopy/rsync_dir/rcheck SSH wrappers, dry-run aware
- scripts/onboard/steps/00-preflight.sh: read-only fact collection (arch, RAM, disk, docker,
  tailscale, MagicMirror runtime, swap), human report + machine YAML snippet
- scripts/onboard/steps/10-50: stub files with TODO headers, no mutations
- hosts/lustro/node.yaml: LUSTRO edge node draft (KEN, role=edge, deploy_autonomy=true,
  git_control=false); hardware fields marked TODO for preflight population

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 14:23:21 +02:00

145 lines
5.5 KiB
Bash
Executable file

#!/usr/bin/env bash
# scripts/onboard/steps/00-preflight.sh — READ-ONLY remote node discovery
#
# Collects facts from the remote node and prints:
# 1. A human-readable report block
# 2. A machine-readable YAML snippet ready to paste into hosts/<node>/node.yaml
#
# NO mutations are performed on the remote host.
# Depends on: lib/common.sh (sourced by orchestrator), lib/remote.sh (sourced here)
set -euo pipefail
STEP_NAME="00-preflight"
# remote.sh is sourced here so individual steps can also be run standalone
# (when REPO_ROOT is in the environment).
: "${REPO_ROOT:?REPO_ROOT is not set — run via onboard.sh}"
# shellcheck source=../lib/remote.sh
source "${REPO_ROOT}/scripts/onboard/lib/remote.sh"
step "[$STEP_NAME] Collecting facts from ${ONBOARD_SSH_USER}@${ONBOARD_SSH_HOST} (read-only)"
# ── gather all facts in a single SSH session ──────────────────────────────────
raw=$(rrun bash -s <<'REMOTE'
set -euo pipefail
# arch / bitness
arch=$(uname -m)
bits=$(getconf LONG_BIT)
# RAM (kB → MB)
mem_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')
mem_mb=$(( mem_kb / 1024 ))
# disk root
disk_root=$(df -h / | awk 'NR==2{print $2" total, "$3" used, "$4" free ("$5" used)"}')
# docker
docker_present=false
docker_info=""
if command -v docker >/dev/null 2>&1; then
docker_present=true
docker_info=$(docker info --format '{{.ServerVersion}}' 2>/dev/null || echo "unknown")
fi
# tailscale
tailscale_present=false
tailscale_status=""
if command -v tailscale >/dev/null 2>&1; then
tailscale_present=true
tailscale_status=$(tailscale status --json 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('BackendState','unknown'))" 2>/dev/null || tailscale status 2>/dev/null | head -1 || echo "unknown")
fi
# Magic Mirror runtime detection
mm_runtime="none"
if systemctl is-active --quiet MagicMirror 2>/dev/null || systemctl is-active --quiet magicmirror 2>/dev/null; then
mm_runtime="systemd"
elif command -v pm2 >/dev/null 2>&1 && pm2 list 2>/dev/null | grep -qi "MagicMirror"; then
mm_runtime="pm2"
elif pgrep -fa "MagicMirror" >/dev/null 2>&1; then
mm_runtime="process"
fi
# swap
swap_current="none"
if command -v swapon >/dev/null 2>&1; then
swap_lines=$(swapon --show --noheadings 2>/dev/null || true)
if [[ -n "$swap_lines" ]]; then
swap_current="$swap_lines"
fi
fi
if command -v zramctl >/dev/null 2>&1; then
zram_lines=$(zramctl --noheadings 2>/dev/null || true)
[[ -n "$zram_lines" ]] && swap_current="${swap_current:+$swap_current; }zram: $zram_lines"
fi
# hostname / os
hostname=$(hostname -f 2>/dev/null || hostname)
os_pretty=$(grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d '"' || echo "unknown")
cat <<EOF
ARCH=$arch
BITS=$bits
MEM_MB=$mem_mb
DISK_ROOT=$disk_root
DOCKER_PRESENT=$docker_present
DOCKER_VERSION=$docker_info
TAILSCALE_PRESENT=$tailscale_present
TAILSCALE_STATUS=$tailscale_status
MM_RUNTIME=$mm_runtime
SWAP_CURRENT=$swap_current
HOSTNAME=$hostname
OS=$os_pretty
EOF
REMOTE
)
# ── parse key=value output ────────────────────────────────────────────────────
_val() { echo "$raw" | grep "^${1}=" | head -1 | cut -d= -f2-; }
arch=$(_val ARCH)
bits=$(_val BITS)
mem_mb=$(_val MEM_MB)
disk_root=$(_val DISK_ROOT)
docker_present=$(_val DOCKER_PRESENT)
docker_version=$(_val DOCKER_VERSION)
tailscale_present=$(_val TAILSCALE_PRESENT)
tailscale_status=$(_val TAILSCALE_STATUS)
mm_runtime=$(_val MM_RUNTIME)
swap_current=$(_val SWAP_CURRENT)
remote_hostname=$(_val HOSTNAME)
os_pretty=$(_val OS)
# ── human-readable report ─────────────────────────────────────────────────────
echo ""
echo "┌─────────────────────────────────────────────────────┐"
printf "│ Preflight report: %-33s│\n" "${ONBOARD_SSH_HOST}"
echo "├─────────────────────────────────────────────────────┤"
printf "│ hostname : %-35s│\n" "$remote_hostname"
printf "│ OS : %-35s│\n" "$os_pretty"
printf "│ arch : %-35s│\n" "${arch} (${bits}-bit)"
printf "│ RAM : %-35s│\n" "${mem_mb} MB"
printf "│ disk / : %-35s│\n" "$disk_root"
printf "│ docker : %-35s│\n" "${docker_present} (v${docker_version})"
printf "│ tailscale : %-35s│\n" "${tailscale_present} / ${tailscale_status}"
printf "│ MagicMirror : %-35s│\n" "$mm_runtime"
printf "│ swap : %-35s│\n" "${swap_current:-none}"
echo "└─────────────────────────────────────────────────────┘"
echo ""
# ── machine-readable YAML snippet ────────────────────────────────────────────
echo "# ── paste into hosts/${NODE_NAME,,}/node.yaml ──"
cat <<YAML
hardware:
arch: ${arch}
ram_mb: ${mem_mb}
swap: ${swap_current:-none}
docker_present: ${docker_present}
docker_version: "${docker_version}"
tailscale_status: "${tailscale_status}"
mm_runtime: ${mm_runtime}
YAML
log "[$STEP_NAME] done — no changes made to remote host"