#!/usr/bin/env bash # migrate-check.sh — validate that a machine meets the requirements to RESTORE # and RUN the Ulicraft stack. Run it on the TARGET host (e.g. oxps) before a # migration, or against a remote host with --host. # # Two tiers of checks: # [restore] hard requirements — without these migrate-import.sh cannot run. # Any failure exits non-zero. # [deploy] needed to build + serve the full stack (update-all.sh, TLS). # Missing ones WARN only — you can restore now, install later. # # Usage: # tooling/migrate-check.sh # check THIS machine # tooling/migrate-check.sh --host oxps # check a remote host over ssh # # The --host form streams this very script to the remote and runs it there, so # nothing needs to be installed on the remote first. set -uo pipefail # NOT -e: we want to run every check and total the failures # ── remote mode: ship self to the host and run it there ───────────── if [ "${1:-}" = "--host" ]; then [ -n "${2:-}" ] || { echo "usage: $0 --host " >&2; exit 2; } exec ssh "$2" 'bash -s' < "$0" fi log() { printf '\n\033[1;32m==>\033[0m %s\n' "$*"; } ok() { printf ' \033[1;32m✓\033[0m %s\n' "$*"; } bad() { printf ' \033[1;31m✗\033[0m %s\n' "$*"; } warn() { printf ' \033[1;33m!\033[0m %s\n' "$*"; } HARD_FAIL=0 SOFT_FAIL=0 # require CMD "human hint" — hard [restore] check require() { if command -v "$1" >/dev/null 2>&1; then ok "$1 present"; else bad "$1 MISSING — $2"; HARD_FAIL=$((HARD_FAIL+1)); fi } # want CMD "human hint" — soft [deploy] check want() { if command -v "$1" >/dev/null 2>&1; then ok "$1 present"; else warn "$1 missing — $2"; SOFT_FAIL=$((SOFT_FAIL+1)); fi } log "[restore] core tools" require docker "install Docker Engine" require git "install git" require tar "install tar" require gzip "install gzip" require ssh "install openssh-client (used to pull the bundle)" log "[restore] docker daemon reachable" if docker info >/dev/null 2>&1; then ok "docker daemon up"; else bad "cannot talk to the docker daemon (permission? is it running?)"; HARD_FAIL=$((HARD_FAIL+1)); fi log "[restore] docker compose v2 plugin" # The stack is authored for Compose v2 (docker compose ...). The legacy v1 # docker-compose binary is NOT accepted — it mangles the project name and # diverges on --build. if cv="$(docker compose version --short 2>/dev/null)"; then major="${cv%%.*}" if [ "${major:-0}" -ge 2 ] 2>/dev/null; then ok "docker compose v$cv" else bad "docker compose reports v$cv — need v2+"; HARD_FAIL=$((HARD_FAIL+1)) fi else bad "'docker compose' v2 plugin not found. Install it: mkdir -p ~/.docker/cli-plugins curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-\$(uname -m) \\ -o ~/.docker/cli-plugins/docker-compose chmod +x ~/.docker/cli-plugins/docker-compose (the legacy 'docker-compose' v1 binary is NOT accepted)" HARD_FAIL=$((HARD_FAIL+1)) fi log "[deploy] build + config tools" want envsubst "part of gettext — render-config.sh / render-nginx.sh need it" want rsync "used to publish the distribution repo" want openssl "used to mint FILES_PASSWORD_HASH" want jq "sync-server-mods.sh falls back to python3 if absent" want node "landing build (pnpm build) — install via nvm" want pnpm "landing build — never npm (see project rules)" log "[deploy] python >= 3.11 (build-modlist.py)" PY="" for c in python3.13 python3.12 python3.11 python3; do if command -v "$c" >/dev/null 2>&1; then v="$($c -c 'import sys;print("%d.%d"%sys.version_info[:2])' 2>/dev/null)" case "$v" in 3.1[1-9]|3.[2-9]*) PY="$c $v"; break;; esac fi done if [ -n "$PY" ]; then ok "python $PY"; else warn "no python >= 3.11 found — build-modlist.py will refuse"; SOFT_FAIL=$((SOFT_FAIL+1)); fi log "[deploy] TLS front (host nginx + acme.sh)" want nginx "host nginx terminates TLS in front of caddy" if [ -x "$HOME/.acme.sh/acme.sh" ] || command -v acme.sh >/dev/null 2>&1; then ok "acme.sh present" else warn "acme.sh missing — needed to RENEW certs (bundle covers the first ~90d): curl https://get.acme.sh | sh -s email=you@example.com" SOFT_FAIL=$((SOFT_FAIL+1)) fi log "[deploy] memory (MAX_MEMORY: 10G in compose)" if command -v free >/dev/null 2>&1; then mem_g="$(free -g | awk '/^Mem:/{print $2}')" if [ "${mem_g:-0}" -ge 12 ] 2>/dev/null; then ok "${mem_g}G RAM"; else warn "only ${mem_g}G RAM — the JVM alone wants 10G; lower MAX_MEMORY or add RAM"; SOFT_FAIL=$((SOFT_FAIL+1)); fi else warn "cannot read RAM (no 'free')"; SOFT_FAIL=$((SOFT_FAIL+1)) fi log "[deploy] free disk on / (need ~10G for images + volumes)" if command -v df >/dev/null 2>&1; then free_g="$(df -BG --output=avail / 2>/dev/null | awk 'NR==2{gsub("G","");print $1}')" if [ "${free_g:-0}" -ge 10 ] 2>/dev/null; then ok "${free_g}G free on /"; else warn "only ${free_g}G free on / — images + volumes want ~10G"; SOFT_FAIL=$((SOFT_FAIL+1)); fi fi log "[deploy] ports free (server + ingress)" if command -v ss >/dev/null 2>&1; then for p in 25565 80 443 "${CADDY_HTTP_PORT:-8880}"; do if ss -ltn "( sport = :$p )" 2>/dev/null | grep -q ":$p"; then warn "tcp/$p already in use — will collide unless it's this stack"; SOFT_FAIL=$((SOFT_FAIL+1)) else ok "tcp/$p free"; fi done if ss -lun "( sport = :24454 )" 2>/dev/null | grep -q ":24454"; then warn "udp/24454 already in use (Simple Voice Chat)"; SOFT_FAIL=$((SOFT_FAIL+1)) else ok "udp/24454 free"; fi else warn "cannot check ports (no 'ss')"; SOFT_FAIL=$((SOFT_FAIL+1)) fi # ── verdict ───────────────────────────────────────────────────────── printf '\n' if [ "$HARD_FAIL" -gt 0 ]; then printf '\033[1;31m✗ %d restore-blocking requirement(s) missing — fix before migrate-import.sh\033[0m\n' "$HARD_FAIL" [ "$SOFT_FAIL" -gt 0 ] && printf '\033[1;33m (+%d deploy warning(s))\033[0m\n' "$SOFT_FAIL" exit 1 fi if [ "$SOFT_FAIL" -gt 0 ]; then printf '\033[1;33m✓ restore requirements met, but %d deploy warning(s) — you can import now, but\n install the missing pieces before ./update-all.sh --hard\033[0m\n' "$SOFT_FAIL" exit 0 fi printf '\033[1;32m✓ all checks passed — ready to migrate\033[0m\n'