feat(tooling): host migration scripts (export/import/check)
Move the stack to a new host keeping the same domain, carrying Drasl users + skins (+ kuma monitors, filestash index) but not the world. - migrate-check.sh: validate the target meets requirements. [restore] hard-fails (docker, compose v2, git, tar, ssh); [deploy] warns (node/pnpm, py>=3.11, nginx, acme.sh, RAM, disk, ports). --host runs it remotely by streaming itself over ssh. - migrate-export.sh: workstation-run, pulls the source read-only over ssh into one 0600 bundle (.env, certs/, drasl_state/kuma_data/ filestash_state volumes, checksummed MANIFEST). Certs in by default (--without-certs); world opt-in (--with-world). - migrate-import.sh: target-run, sha256-verify, refuse existing volumes (--force), backup+restore .env with --path-prefix rewrite, check host paths resolve, restore certs+volumes, print the manual tail. - plan/22-migration.md: human walkthrough (cochi -> oxps), rollback window warning, rehearsal notes. - .gitignore: ignore ulicraft-bundle-*.tar.gz (holds secrets + LE keys). Assumes Compose v2 on the target (no update-all.sh changes). Distribution repo, TLS issuance, DNS and firewall stay manual. Round-trip tested against prod: 9.2M bundle, drasl.db restored byte-exact, clobber guard verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
00e9815307
commit
b9bcc13b73
5 files changed
+625
No files matched your search
Executable
+147
@@ -0,0 +1,147 @@
|
||||
#!/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 <ssh-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'
|
||||
Reference in new issue
Block a user