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>
This commit is contained in:
parent
58ac6edd7d
commit
adb84079ab
26
hosts/lustro/node.yaml
Normal file
26
hosts/lustro/node.yaml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# hosts/lustro/node.yaml — LUSTRO edge node manifest
|
||||
# Run scripts/onboard/onboard.sh --node lustro --step 00-preflight
|
||||
# to auto-populate the TODO fields below.
|
||||
|
||||
name: LUSTRO
|
||||
role: edge
|
||||
location: KEN
|
||||
|
||||
ssh_user: oskar
|
||||
|
||||
tailscale:
|
||||
hostname: lustro
|
||||
# ip: TODO — fill after tailscale join (step 30)
|
||||
|
||||
deploy_autonomy: true # onboard.sh may run mutating steps autonomously
|
||||
git_control: false # node does NOT pull from Forgejo directly; push-based
|
||||
|
||||
hardware:
|
||||
arch: TODO # populated by 00-preflight (e.g. aarch64, x86_64)
|
||||
ram_mb: TODO # populated by 00-preflight
|
||||
swap: TODO # populated by 00-preflight (none / <size>)
|
||||
mm_runtime: TODO # populated by 00-preflight (systemd / pm2 / process / none)
|
||||
|
||||
services:
|
||||
node-agent:
|
||||
runtime: TODO # populated by 40-deploy-node-agent (image tag, config hash)
|
||||
64
scripts/onboard/lib/common.sh
Normal file
64
scripts/onboard/lib/common.sh
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/onboard/lib/common.sh — shared helpers for the onboarding tool
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── colour codes (disabled when not a tty) ──────────────────────────────────
|
||||
if [[ -t 1 ]]; then
|
||||
_C_RESET='\033[0m'
|
||||
_C_GREEN='\033[0;32m'
|
||||
_C_YELLOW='\033[1;33m'
|
||||
_C_RED='\033[0;31m'
|
||||
_C_CYAN='\033[0;36m'
|
||||
_C_BOLD='\033[1m'
|
||||
else
|
||||
_C_RESET='' _C_GREEN='' _C_YELLOW='' _C_RED='' _C_CYAN='' _C_BOLD=''
|
||||
fi
|
||||
|
||||
# ── logging ──────────────────────────────────────────────────────────────────
|
||||
log() { echo -e "${_C_GREEN}[onboard]${_C_RESET} $(date +'%H:%M:%S') ${*}"; }
|
||||
warn() { echo -e "${_C_YELLOW}[WARN]${_C_RESET} $(date +'%H:%M:%S') ${*}" >&2; }
|
||||
die() { echo -e "${_C_RED}[ERROR]${_C_RESET} $(date +'%H:%M:%S') ${*}" >&2; exit 1; }
|
||||
step() { echo -e "${_C_CYAN}${_C_BOLD}==> ${*}${_C_RESET}"; }
|
||||
dryrun() { echo -e "${_C_YELLOW}[dry-run]${_C_RESET} ${*}"; }
|
||||
|
||||
# ── command detection ─────────────────────────────────────────────────────────
|
||||
have_cmd() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
# ── file helpers ──────────────────────────────────────────────────────────────
|
||||
# ensure_line FILE LINE — appends LINE to FILE if it is not already present (idempotent)
|
||||
ensure_line() {
|
||||
local file="$1" line="$2"
|
||||
[[ -f "$file" ]] || touch "$file"
|
||||
grep -qxF "$line" "$file" || echo "$line" >> "$file"
|
||||
}
|
||||
|
||||
# ── node.yaml parsing ─────────────────────────────────────────────────────────
|
||||
# require_node_yaml NODE — sets NODE_YAML; exits if not found
|
||||
require_node_yaml() {
|
||||
local node="$1"
|
||||
NODE_YAML="${REPO_ROOT}/hosts/${node,,}/node.yaml"
|
||||
[[ -f "$NODE_YAML" ]] || die "node.yaml not found: $NODE_YAML"
|
||||
export NODE_YAML
|
||||
}
|
||||
|
||||
# yaml_get NODE_YAML KEY — read a scalar value from a YAML file
|
||||
# Uses yq when available; falls back to grep/sed for simple key: value pairs.
|
||||
# Supports dot-separated paths (e.g. tailscale.hostname) only in yq mode;
|
||||
# the grep fallback handles only the last path component.
|
||||
yaml_get() {
|
||||
local file="$1" key="$2"
|
||||
if have_cmd yq; then
|
||||
yq -r ".${key} // empty" "$file" 2>/dev/null
|
||||
else
|
||||
# fallback: extract last segment of key, match " key: value"
|
||||
local leaf="${key##*.}"
|
||||
grep -E "^\s*${leaf}:" "$file" | head -1 | sed 's/.*: *//' | tr -d '"' | tr -d "'"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── git wrapper ────────────────────────────────────────────────────────────────
|
||||
# All git calls from onboarding scripts must go through this so --no-pager is
|
||||
# always set and there is no interactive output.
|
||||
git() { command git --no-pager "$@"; }
|
||||
export -f git
|
||||
51
scripts/onboard/lib/remote.sh
Normal file
51
scripts/onboard/lib/remote.sh
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/onboard/lib/remote.sh — SSH helpers for remote node operations
|
||||
# Requires: ONBOARD_SSH_USER, ONBOARD_SSH_HOST to be set by the caller.
|
||||
# Inherits: DRY_RUN (boolean string "true"/"false")
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${ONBOARD_SSH_USER:?remote.sh: ONBOARD_SSH_USER is not set}"
|
||||
: "${ONBOARD_SSH_HOST:?remote.sh: ONBOARD_SSH_HOST is not set}"
|
||||
: "${DRY_RUN:=false}"
|
||||
|
||||
_SSH_OPTS=(
|
||||
-o StrictHostKeyChecking=accept-new
|
||||
-o ConnectTimeout=10
|
||||
-o BatchMode=yes
|
||||
)
|
||||
|
||||
# rrun CMD [ARGS…] — run a command on the remote node via SSH
|
||||
rrun() {
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
dryrun "ssh ${ONBOARD_SSH_USER}@${ONBOARD_SSH_HOST} -- $*"
|
||||
return 0
|
||||
fi
|
||||
ssh "${_SSH_OPTS[@]}" "${ONBOARD_SSH_USER}@${ONBOARD_SSH_HOST}" -- "$@"
|
||||
}
|
||||
|
||||
# rcopy LOCAL_PATH REMOTE_PATH — copy a file to the remote node via scp
|
||||
rcopy() {
|
||||
local src="$1" dst="$2"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
dryrun "scp $src ${ONBOARD_SSH_USER}@${ONBOARD_SSH_HOST}:$dst"
|
||||
return 0
|
||||
fi
|
||||
scp "${_SSH_OPTS[@]}" "$src" "${ONBOARD_SSH_USER}@${ONBOARD_SSH_HOST}:$dst"
|
||||
}
|
||||
|
||||
# rsync_dir LOCAL_DIR REMOTE_DIR [EXTRA_RSYNC_ARGS…]
|
||||
rsync_dir() {
|
||||
local src="$1" dst="$2"
|
||||
shift 2
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
dryrun "rsync -az $src ${ONBOARD_SSH_USER}@${ONBOARD_SSH_HOST}:$dst"
|
||||
return 0
|
||||
fi
|
||||
rsync -az -e "ssh ${_SSH_OPTS[*]}" "$src" "${ONBOARD_SSH_USER}@${ONBOARD_SSH_HOST}:$dst" "$@"
|
||||
}
|
||||
|
||||
# rcheck — verify SSH connectivity; returns 0 if reachable
|
||||
rcheck() {
|
||||
ssh "${_SSH_OPTS[@]}" -o ConnectTimeout=5 "${ONBOARD_SSH_USER}@${ONBOARD_SSH_HOST}" -- true 2>/dev/null
|
||||
}
|
||||
187
scripts/onboard/onboard.sh
Executable file
187
scripts/onboard/onboard.sh
Executable file
|
|
@ -0,0 +1,187 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/onboard/onboard.sh — node onboarding orchestrator
|
||||
#
|
||||
# Usage:
|
||||
# onboard.sh --node <name> [--step <name>] [--from <step>] [--dry-run]
|
||||
#
|
||||
# Flags:
|
||||
# --node <name> node name matching hosts/<name>/node.yaml (required)
|
||||
# --step <name> run only this step (e.g. 00-preflight)
|
||||
# --from <step> start from this step, run all subsequent steps
|
||||
# --dry-run print what would be done without mutating anything
|
||||
#
|
||||
# Steps run in lexicographic order from scripts/onboard/steps/.
|
||||
# Steps that require deploy_autonomy=true are skipped (with a warning) when
|
||||
# that flag is false in node.yaml. Steps that require git_control=true are
|
||||
# similarly gated.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
STEPS_DIR="${REPO_ROOT}/scripts/onboard/steps"
|
||||
LIB_DIR="${REPO_ROOT}/scripts/onboard/lib"
|
||||
|
||||
# ── load helpers ──────────────────────────────────────────────────────────────
|
||||
# shellcheck source=lib/common.sh
|
||||
source "${LIB_DIR}/common.sh"
|
||||
|
||||
# ── defaults ──────────────────────────────────────────────────────────────────
|
||||
NODE_NAME=""
|
||||
ONLY_STEP=""
|
||||
FROM_STEP=""
|
||||
DRY_RUN="false"
|
||||
export DRY_RUN REPO_ROOT
|
||||
|
||||
# ── argument parsing ──────────────────────────────────────────────────────────
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
Usage: onboard.sh --node <name> [--step <name>] [--from <step>] [--dry-run]
|
||||
|
||||
--node <name> node name matching hosts/<name>/node.yaml (required)
|
||||
--step <name> run only this single step (e.g. 00-preflight)
|
||||
--from <step> start from this step, continue to end
|
||||
--dry-run no mutations; show what would run
|
||||
|
||||
Examples:
|
||||
onboard.sh --node lustro
|
||||
onboard.sh --node lustro --step 00-preflight
|
||||
onboard.sh --node lustro --from 20-install-docker
|
||||
onboard.sh --node lustro --dry-run
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--node) NODE_NAME="${2:?--node requires a value}"; shift 2 ;;
|
||||
--step) ONLY_STEP="${2:?--step requires a value}"; shift 2 ;;
|
||||
--from) FROM_STEP="${2:?--from requires a value}"; shift 2 ;;
|
||||
--dry-run) DRY_RUN="true"; shift ;;
|
||||
-h|--help) usage ;;
|
||||
*) die "Unknown argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$NODE_NAME" ]] && { warn "--node is required"; usage; }
|
||||
|
||||
export NODE_NAME
|
||||
|
||||
# ── load node.yaml ────────────────────────────────────────────────────────────
|
||||
require_node_yaml "$NODE_NAME"
|
||||
|
||||
log "Loading manifest: $NODE_YAML"
|
||||
|
||||
DEPLOY_AUTONOMY=$(yaml_get "$NODE_YAML" "deploy_autonomy")
|
||||
GIT_CONTROL=$(yaml_get "$NODE_YAML" "git_control")
|
||||
SSH_USER=$(yaml_get "$NODE_YAML" "ssh_user")
|
||||
TS_HOSTNAME=$(yaml_get "$NODE_YAML" "tailscale.hostname")
|
||||
|
||||
DEPLOY_AUTONOMY="${DEPLOY_AUTONOMY:-false}"
|
||||
GIT_CONTROL="${GIT_CONTROL:-false}"
|
||||
|
||||
[[ -z "$SSH_USER" ]] && die "ssh_user not set in $NODE_YAML"
|
||||
[[ -z "$TS_HOSTNAME" ]] && die "tailscale.hostname not set in $NODE_YAML"
|
||||
|
||||
export ONBOARD_SSH_USER="$SSH_USER"
|
||||
export ONBOARD_SSH_HOST="$TS_HOSTNAME"
|
||||
|
||||
log "Node: ${NODE_NAME} | host: ${TS_HOSTNAME} | user: ${SSH_USER}"
|
||||
log "deploy_autonomy=${DEPLOY_AUTONOMY} git_control=${GIT_CONTROL} dry_run=${DRY_RUN}"
|
||||
|
||||
# ── collect steps ─────────────────────────────────────────────────────────────
|
||||
# Steps are NN-name.sh files in lexicographic order.
|
||||
mapfile -t ALL_STEPS < <(find "$STEPS_DIR" -maxdepth 1 -name '[0-9][0-9]-*.sh' | sort)
|
||||
|
||||
if [[ ${#ALL_STEPS[@]} -eq 0 ]]; then
|
||||
die "No steps found in $STEPS_DIR"
|
||||
fi
|
||||
|
||||
# Determine which steps to run based on flags.
|
||||
declare -a STEPS_TO_RUN=()
|
||||
|
||||
for step_path in "${ALL_STEPS[@]}"; do
|
||||
step_file=$(basename "$step_path" .sh)
|
||||
|
||||
if [[ -n "$ONLY_STEP" ]]; then
|
||||
# Match on prefix (e.g. "00-preflight" matches "00-preflight.sh")
|
||||
[[ "$step_file" == "$ONLY_STEP" ]] || continue
|
||||
elif [[ -n "$FROM_STEP" ]]; then
|
||||
# Skip steps before FROM_STEP
|
||||
[[ "$step_file" < "$FROM_STEP" && "$step_file" != "$FROM_STEP" ]] && continue
|
||||
fi
|
||||
|
||||
STEPS_TO_RUN+=("$step_path")
|
||||
done
|
||||
|
||||
if [[ ${#STEPS_TO_RUN[@]} -eq 0 ]]; then
|
||||
die "No matching steps found (--step='${ONLY_STEP}' --from='${FROM_STEP}')"
|
||||
fi
|
||||
|
||||
log "Steps to run (${#STEPS_TO_RUN[@]}):"
|
||||
for s in "${STEPS_TO_RUN[@]}"; do
|
||||
printf " %s\n" "$(basename "$s")"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# ── step execution loop ───────────────────────────────────────────────────────
|
||||
# Steps that start at 10+ are "mutating" and require deploy_autonomy=true.
|
||||
# Steps that start at 30+ and deal with git/repo sync require git_control=true.
|
||||
# Step 00-preflight is always allowed (read-only).
|
||||
|
||||
_step_needs_autonomy() {
|
||||
local num="${1%%[^0-9]*}" # leading digits
|
||||
[[ "$num" -ge 10 ]] 2>/dev/null
|
||||
}
|
||||
|
||||
_step_needs_git_control() {
|
||||
local name="$1"
|
||||
[[ "$name" == *"git"* || "$name" == *"repo"* || "$name" == *"clone"* ]]
|
||||
}
|
||||
|
||||
FAILED_STEPS=()
|
||||
|
||||
for step_path in "${STEPS_TO_RUN[@]}"; do
|
||||
step_file=$(basename "$step_path" .sh)
|
||||
step_num="${step_file%%[^0-9]*}"
|
||||
|
||||
# autonomy gate
|
||||
if _step_needs_autonomy "$step_num" && [[ "$DEPLOY_AUTONOMY" != "true" ]]; then
|
||||
warn "Skipping $step_file — deploy_autonomy=false in $NODE_YAML"
|
||||
warn "Run this step manually or set deploy_autonomy: true"
|
||||
continue
|
||||
fi
|
||||
|
||||
# git_control gate
|
||||
if _step_needs_git_control "$step_file" && [[ "$GIT_CONTROL" != "true" ]]; then
|
||||
warn "Skipping $step_file — git_control=false in $NODE_YAML"
|
||||
continue
|
||||
fi
|
||||
|
||||
step "Running: $step_file"
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
dryrun "Would execute: $step_path"
|
||||
continue
|
||||
fi
|
||||
|
||||
if bash "$step_path"; then
|
||||
log "$step_file — OK"
|
||||
else
|
||||
rc=$?
|
||||
warn "$step_file — FAILED (exit $rc)"
|
||||
FAILED_STEPS+=("$step_file")
|
||||
fi
|
||||
|
||||
echo ""
|
||||
done
|
||||
|
||||
# ── summary ───────────────────────────────────────────────────────────────────
|
||||
if [[ ${#FAILED_STEPS[@]} -gt 0 ]]; then
|
||||
die "Onboarding finished with failures: ${FAILED_STEPS[*]}"
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log "Dry-run complete — no changes made."
|
||||
else
|
||||
log "All steps completed successfully for node ${NODE_NAME}."
|
||||
fi
|
||||
144
scripts/onboard/steps/00-preflight.sh
Executable file
144
scripts/onboard/steps/00-preflight.sh
Executable file
|
|
@ -0,0 +1,144 @@
|
|||
#!/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"
|
||||
14
scripts/onboard/steps/10-bootstrap-runtime.sh
Executable file
14
scripts/onboard/steps/10-bootstrap-runtime.sh
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/onboard/steps/10-bootstrap-runtime.sh — create /opt/homelab layout on remote node
|
||||
#
|
||||
# TODO: create /opt/homelab/{data,config,logs,state,events,world,actions/{pending,approved,running,completed,failed}}
|
||||
# TODO: set ownership to ssh_user (from node.yaml)
|
||||
# TODO: write /opt/homelab/state/node_name from node.yaml name field
|
||||
# TODO: idempotent — skip dirs that already exist
|
||||
|
||||
set -euo pipefail
|
||||
: "${REPO_ROOT:?REPO_ROOT is not set — run via onboard.sh}"
|
||||
source "${REPO_ROOT}/scripts/onboard/lib/remote.sh"
|
||||
|
||||
STEP_NAME="10-bootstrap-runtime"
|
||||
step "[$STEP_NAME] TODO — not yet implemented"
|
||||
16
scripts/onboard/steps/20-install-docker.sh
Executable file
16
scripts/onboard/steps/20-install-docker.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/onboard/steps/20-install-docker.sh — install Docker Engine on remote node
|
||||
#
|
||||
# TODO: skip if docker already present (check from 00-preflight facts or live rrun)
|
||||
# TODO: detect distro (Debian/Ubuntu/Raspberry Pi OS) and use appropriate apt repo
|
||||
# TODO: install docker-ce, docker-ce-cli, containerd.io
|
||||
# TODO: add ssh_user to docker group
|
||||
# TODO: enable + start docker.service
|
||||
# TODO: gate on deploy_autonomy=true in node.yaml (skip step if false, warn operator)
|
||||
|
||||
set -euo pipefail
|
||||
: "${REPO_ROOT:?REPO_ROOT is not set — run via onboard.sh}"
|
||||
source "${REPO_ROOT}/scripts/onboard/lib/remote.sh"
|
||||
|
||||
STEP_NAME="20-install-docker"
|
||||
step "[$STEP_NAME] TODO — not yet implemented"
|
||||
16
scripts/onboard/steps/30-install-tailscale.sh
Executable file
16
scripts/onboard/steps/30-install-tailscale.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/onboard/steps/30-install-tailscale.sh — install and join Tailscale on remote node
|
||||
#
|
||||
# TODO: skip if tailscale already installed and connected
|
||||
# TODO: install via https://tailscale.com/install.sh (or distro pkg)
|
||||
# TODO: gate on operator-provided auth key (TAILSCALE_AUTH_KEY env var; never hardcode)
|
||||
# TODO: tailscale up --auth-key=$TAILSCALE_AUTH_KEY --hostname=<node.yaml name>
|
||||
# TODO: verify node appears in tailscale status within timeout
|
||||
# TODO: gate on deploy_autonomy=true in node.yaml
|
||||
|
||||
set -euo pipefail
|
||||
: "${REPO_ROOT:?REPO_ROOT is not set — run via onboard.sh}"
|
||||
source "${REPO_ROOT}/scripts/onboard/lib/remote.sh"
|
||||
|
||||
STEP_NAME="30-install-tailscale"
|
||||
step "[$STEP_NAME] TODO — not yet implemented"
|
||||
16
scripts/onboard/steps/40-deploy-node-agent.sh
Executable file
16
scripts/onboard/steps/40-deploy-node-agent.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/onboard/steps/40-deploy-node-agent.sh — deploy node-agent to remote node
|
||||
#
|
||||
# TODO: rsync services/node-agent/ and hosts/<node>/runtime/node-agent/ to remote
|
||||
# TODO: populate /opt/homelab/config/node-agent/.env from env.example + operator-provided secrets
|
||||
# TODO: docker compose up -d (or docker-compose for CHELSTY nodes using v1)
|
||||
# TODO: wait for healthcheck to pass
|
||||
# TODO: emit deployment_completed event via scripts/lib/events.sh
|
||||
# TODO: gate on git_control flag — if false, skip and print manual instructions
|
||||
|
||||
set -euo pipefail
|
||||
: "${REPO_ROOT:?REPO_ROOT is not set — run via onboard.sh}"
|
||||
source "${REPO_ROOT}/scripts/onboard/lib/remote.sh"
|
||||
|
||||
STEP_NAME="40-deploy-node-agent"
|
||||
step "[$STEP_NAME] TODO — not yet implemented"
|
||||
16
scripts/onboard/steps/50-verify.sh
Executable file
16
scripts/onboard/steps/50-verify.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/onboard/steps/50-verify.sh — end-to-end verification of onboarded node
|
||||
#
|
||||
# TODO: rcheck SSH reachability
|
||||
# TODO: rrun docker ps — confirm node-agent container is running
|
||||
# TODO: check /opt/homelab/state/heartbeat timestamp is recent (< 5 min)
|
||||
# TODO: verify node appears in Observer world state (/opt/homelab/world/nodes.json on control node)
|
||||
# TODO: run services/<service>/healthcheck.sh for each enabled service
|
||||
# TODO: print pass/fail summary table; exit 1 if any check failed
|
||||
|
||||
set -euo pipefail
|
||||
: "${REPO_ROOT:?REPO_ROOT is not set — run via onboard.sh}"
|
||||
source "${REPO_ROOT}/scripts/onboard/lib/remote.sh"
|
||||
|
||||
STEP_NAME="50-verify"
|
||||
step "[$STEP_NAME] TODO — not yet implemented"
|
||||
Loading…
Reference in a new issue