From d81ac27ebb851efa159c934fb4dc9ba511fed72c Mon Sep 17 00:00:00 2001 From: Oskar Kapala Date: Tue, 9 Jun 2026 12:21:53 +0200 Subject: [PATCH] =?UTF-8?q?feat(onboard):=20implement=2020-base.sh=20for?= =?UTF-8?q?=20LUSTRO=20=E2=80=94=20swap=E2=86=92zram,=20/opt/homelab,=20ev?= =?UTF-8?q?ent=20dir?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three idempotent stages with guards (probe-before-mutate), rrun() for all remote mutations, rprobe() for unconditional state queries. Reads hardware.swap.mb from node.yaml (default 2048 MB). Adds swap.mb: 2048 to hosts/lustro/node.yaml so the value is declarative. Co-Authored-By: Claude Sonnet 4.6 --- hosts/lustro/node.yaml | 1 + scripts/onboard/steps/20-base.sh | 157 +++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 scripts/onboard/steps/20-base.sh diff --git a/hosts/lustro/node.yaml b/hosts/lustro/node.yaml index e7cb92b..ffa703a 100644 --- a/hosts/lustro/node.yaml +++ b/hosts/lustro/node.yaml @@ -21,6 +21,7 @@ hardware: ram_mb: 4096 swap: kind: zram + mb: 2048 docker_present: true mm_runtime: systemd:magicmirror.service diff --git a/scripts/onboard/steps/20-base.sh b/scripts/onboard/steps/20-base.sh new file mode 100644 index 0000000..72e9b41 --- /dev/null +++ b/scripts/onboard/steps/20-base.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# scripts/onboard/steps/20-base.sh — base system configuration for LUSTRO +# +# Stages: +# 1. swap→zram — disable dphys-swapfile, install + configure zram-tools +# 2. /opt/homelab — create base directory, chown : +# 3. event dir — create /opt/homelab/events/, chown -R +# +# Dry-run convention: +# - Probes (state queries) run unconditionally — dry-run reflects real state +# - Mutations use rrun() which skips execution when DRY_RUN=1 + +set -euo pipefail + +STEP_NAME="20-base" + +: "${REPO_ROOT:?REPO_ROOT is not set — run via onboard.sh}" +: "${NODE_YAML:?NODE_YAML is not set — run via onboard.sh}" +: "${DRY_RUN:=0}" + +# Source common.sh when run standalone (orchestrator sources it before calling steps) +if ! declare -f log >/dev/null 2>&1; then + # shellcheck source=../lib/common.sh + source "${REPO_ROOT}/scripts/onboard/lib/common.sh" +fi + +# ── parse node.yaml ─────────────────────────────────────────────────────────── +SSH_USER=$(yaml_get "$NODE_YAML" "ssh_user") +TS_HOSTNAME=$(yaml_get "$NODE_YAML" "tailscale.hostname") +_raw_mb=$(yaml_get "$NODE_YAML" "hardware.swap.mb" 2>/dev/null || true) +SWAP_MB="${_raw_mb:-2048}" + +[[ -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="${ONBOARD_SSH_USER:-${SSH_USER}}" +export ONBOARD_SSH_HOST="${ONBOARD_SSH_HOST:-${TS_HOSTNAME}}" + +# shellcheck source=../lib/remote.sh +source "${REPO_ROOT}/scripts/onboard/lib/remote.sh" + +# ── rprobe: read-only remote probe — always runs, even in dry-run ───────────── +rprobe() { + ssh "${_SSH_OPTS[@]}" "${ONBOARD_SSH_USER}@${ONBOARD_SSH_HOST}" -- "$@" +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# Stage 1 — swap→zram +# ═══════════════════════════════════════════════════════════════════════════════ +step "[$STEP_NAME] 1/3 swap→zram (target: zram ${SWAP_MB} MB, algo=zstd)" + +# Guard: is dphys-swapfile still active? +if rprobe 'systemctl is-active dphys-swapfile' >/dev/null 2>&1; then + log "dphys-swapfile active — disabling" + rrun sudo dphys-swapfile swapoff + rrun sudo systemctl disable --now dphys-swapfile + if rprobe '[ -f /var/swap ]' 2>/dev/null; then + rrun sudo rm -f /var/swap + log "Removed /var/swap" + fi +else + log "dphys-swapfile not active — skip disable" +fi + +# Guard: is zram-tools installed? +if ! rprobe 'command -v zramswap' >/dev/null 2>&1; then + log "zram-tools not found — installing" + rrun sudo apt-get install -y zram-tools +else + log "zram-tools already installed" +fi + +# Guard: is zram already active with the correct SIZE? +_zram_ok=0 +if rprobe 'swapon --show --noheadings 2>/dev/null | grep -q zram' 2>/dev/null; then + _cfg_size=$(rprobe \ + "grep -E '^[[:space:]]*SIZE=' /etc/default/zramswap 2>/dev/null \ + | cut -d= -f2 | tr -d '\"[:space:]'" 2>/dev/null || echo "") + if [[ "$_cfg_size" == "$SWAP_MB" ]]; then + _zram_ok=1 + log "zram active, SIZE=${SWAP_MB} MB — skip configure" + else + log "zram active but SIZE='${_cfg_size:-unset}' ≠ ${SWAP_MB} — reconfigure" + fi +fi + +if [[ "$_zram_ok" -eq 0 ]]; then + log "Writing /etc/default/zramswap (ALGO=zstd, SIZE=${SWAP_MB})" + rrun sudo bash -c "printf 'ALGO=zstd\nSIZE=${SWAP_MB}\n' | tee /etc/default/zramswap > /dev/null" + rrun sudo systemctl enable zramswap + rrun sudo systemctl restart zramswap +fi + +# Verify (skipped in dry-run — mutations may not have run) +if [ "${DRY_RUN:-0}" != 1 ]; then + if rprobe 'swapon --show --noheadings 2>/dev/null | grep -q zram'; then + log "Verify OK: zram swap active" + rprobe 'swapon --show; echo "---"; zramctl' || true + else + die "zram swap not active after setup — check: systemctl status zramswap on ${TS_HOSTNAME}" + fi + if rprobe 'systemctl is-active dphys-swapfile' >/dev/null 2>&1; then + warn "dphys-swapfile still reports active — manual inspection needed" + else + log "Verify OK: dphys-swapfile not active" + fi +fi + +# ═══════════════════════════════════════════════════════════════════════════════ +# Stage 2 — /opt/homelab +# ═══════════════════════════════════════════════════════════════════════════════ +step "[$STEP_NAME] 2/3 /opt/homelab (owner: ${SSH_USER}:${SSH_USER})" + +# Guard: exists AND owned by SSH_USER? +_dir_ok=0 +if rprobe '[ -d /opt/homelab ]' 2>/dev/null; then + _owner=$(rprobe "stat -c '%U' /opt/homelab" 2>/dev/null || echo "") + if [[ "$_owner" == "$SSH_USER" ]]; then + _dir_ok=1 + log "/opt/homelab exists, owner=${SSH_USER} — skip" + else + log "/opt/homelab exists but owner='${_owner}' — fixing" + fi +else + log "/opt/homelab missing — creating" +fi + +if [[ "$_dir_ok" -eq 0 ]]; then + rrun sudo mkdir -p /opt/homelab + rrun sudo chown "${SSH_USER}:${SSH_USER}" /opt/homelab +fi + +# ═══════════════════════════════════════════════════════════════════════════════ +# Stage 3 — event dir +# ═══════════════════════════════════════════════════════════════════════════════ +step "[$STEP_NAME] 3/3 event dir (/opt/homelab/events/${TS_HOSTNAME})" + +# Guard: event subdir exists AND /opt/homelab/events owned by SSH_USER? +_evdir_ok=0 +if rprobe "[ -d /opt/homelab/events/${TS_HOSTNAME} ]" 2>/dev/null; then + _ev_owner=$(rprobe "stat -c '%U' /opt/homelab/events" 2>/dev/null || echo "") + if [[ "$_ev_owner" == "$SSH_USER" ]]; then + _evdir_ok=1 + log "/opt/homelab/events/${TS_HOSTNAME} exists, owner=${SSH_USER} — skip" + else + log "/opt/homelab/events exists but owner='${_ev_owner}' — fixing" + fi +else + log "/opt/homelab/events/${TS_HOSTNAME} missing — creating" +fi + +if [[ "$_evdir_ok" -eq 0 ]]; then + rrun sudo mkdir -p "/opt/homelab/events/${TS_HOSTNAME}" + rrun sudo chown -R "${SSH_USER}:${SSH_USER}" /opt/homelab/events +fi + +log "[$STEP_NAME] done"