Files
g9-migration/g9-migration/scripts/07-migrate-stateful.sh
T

219 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# 07-migrate-stateful.sh — T4.x : export/import des stateful (xpenology → G9)
#
# Garde-fous : G4 (double-run, xpenology = source de vérité, lecture seule), G7 (rollback), G8 (journalisation).
#
# Principe (garde-fou G4 — double-run) :
# 1. EXPORT des données depuis le xpenology (source de vérité) → copie sur le G9.
# 2. VÉRIFICATION D'INTÉGRITÉ fichier par fichier (manifeste sha256 trié, diff).
# 3. Le xpenology n'est JAMAIS modifié/supprimé ici — lecture seule.
# 4. Chaque brique est validée sur le G9 AVANT que le xpenology soit décommissionné (T6.4).
#
# Usage :
# ./07-migrate-stateful.sh <brique>
# briques : vaultwarden | nextcloud | photoprism | gitea | n8n
#
# Prérequis :
# - Accès SSH au xpenology : ssh -p 222 Master@192.168.1.25 (clé SSH + sudo NOPASSWD —
# compte `effi` bloqué par Synology depuis 10/2026, voir journal T1.1-b).
# - Sur le G9 : /data monté, Docker opérationnel.
# - Secrets bootstrap (03b-secrets.sh) déjà exécuté.
#
# ⚠️ TRANSPORT (mesuré 2026-10-11, DSM 7) : le sshd du xpenology refuse la
# commande rsync --server (« Permission denied, please try again. ») et scp
# n'est pas servi (« subsystem request failed »). Le pull rsync/scp depuis .25
# est donc IMPOSSIBLE. Transports qui fonctionnent : « ssh sudo tar cz » et
# « ssh sudo cat ». Les briques sont migrées par les scripts dédiés
# (07a-migrate-gitea.sh, 07b-migrate-vaultwarden.sh, 07c-migrate-nextcloud.sh)
# qui utilisent tar+ssh ; ce script générique est gardé pour les hôtes qui
# exposent rsync.
#
# ⚠️ COHÉRENCE DES BASES (G4) : le rsync d'une base VIVANTE (MariaDB/Postgres en
# service sur .25) produit une copie incohérente et un manifeste sha256 qui
# change pendant le transfert. Le script STOPPE donc les conteneurs source
# (XP_CONTAINERS) avant l'export et les REDÉMARRE juste après — le double-run
# G4 est respecté (la source reprend du service, rien n'est supprimé).
#
# ⚠️ CREDENTIELS DB : l'import est une COPIE de la base existante. Les env
# MARIADB_*/POSTGRES_* (seedés par 03b-secrets.sh) sont IGNORÉS sur une base
# déjà initialisée → la base importée garde les CREDENTIELS D'ORIGINE du
# xpenology, et la config app importée est cohérente avec elle. Le service
# G9 démarre donc avec les identifiants d'origine, PAS les secrets générés.
#
# ⚠️⚠️ À CONFIRMER PENDANT L'INVENTAIRE T1.2 ⚠️⚠️
# Les chemins sources xpenology (XP_MAP) sont des HYPOTHÈSES. Les conteneurs
# stateful ont été créés via la GUI DSM (snapshot ETAT-DE-LART-2026-10-06 §1),
# donc leur arborescence réelle n'est pas garantie. AVANT d'exécuter, vérifier
# sur le xpenology que chaque chemin source existe et contient les bonnes données,
# puis ajuster XP_MAP si besoin. Le script refuse de migrer un chemin source absent.
#
# Rollback : la migration est une COPIE → rien n'est perdu. Sur le G9, supprimer
# le conteneur + les sous-dossiers importés dans compose/<brique>/ si l'import est inacceptable.
set -euo pipefail
log() { echo -e "\n[migrate] $*"; }
journal() {
local action="$1" raison="$2" resultat="$3" rollback="$4"
printf '| %s | 4 | migrate:%s : %s | %s | %s | %s |\n' "$(date '+%Y-%m-%d %H:%M')" "$BRIQUE" "$action" "$raison" "$resultat" "$rollback" >> "$(dirname "$0")/../journal.md"
}
BRIQUE="${1:-}"
[[ -n "$BRIQUE" ]] || { echo "Usage : ./07-migrate-stateful.sh <vaultwarden|nextcloud|photoprism|gitea|n8n>"; exit 1; }
# --- Coordonnées xpenology (lecture seule) ---
XP_USER="Master"
XP_HOST="192.168.1.25"
XP_PORT=222
XP_DOCKER="/var/packages/ContainerManager/target/usr/bin/docker"
XP_SSH="ssh -p $XP_PORT -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600 $XP_USER@$XP_HOST"
# Pré-test rapide : connexion SSH + rsync présent sur le xpenology (échec franc avant transfert).
if ! $XP_SSH "command -v rsync >/dev/null 2>&1" 2>/dev/null; then
echo "ERREUR : connexion SSH impossible vers $XP_USER@$XP_HOST:$XP_PORT ou rsync absent du NAS."
echo "Vérifiez les accès SSH et la présence de rsync côté xpenology avant de relancer."
exit 1
fi
# --- Racine des compose G9 ---
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
COMPOSE_ROOT="$(dirname "$SCRIPT_DIR")/compose"
COMPOSE_DIR="$COMPOSE_ROOT/$BRIQUE"
[[ -d "$COMPOSE_DIR" ]] || { echo "ERREUR : $COMPOSE_DIR absent"; exit 1; }
# --- Mapping source (xpenology) → sous-dossier compose (G9) ---
# Format par brique : "xp_dir:g9_subdir;xp_dir:g9_subdir;..."
# g9_subdir est relatif à compose/<brique>/ (il correspond aux volumes relatifs ./data, ./db, ...).
# ⚠️ Chemins sources = HYPOTHÈSES à confirmer à l'inventaire T1.2.
declare -A XP_MAP=(
[vaultwarden]="/volume1/docker/vaultwarden:data"
[nextcloud]="/volume1/docker/nextcloud/data:data;/volume1/docker/nextcloud/config:config;/volume1/docker/nextcloud_db/db:db"
[photoprism]="/volume1/docker/photoprism/storage:storage;/volume1/docker/photoprism_db/db:db"
[gitea]="/volume1/docker/gitea:data"
[n8n]="/volume1/docker/n8n/data:data;/volume1/docker/n8n_db/db:db"
)
# Conteneurs SOURCE à stopper sur .25 pendant l'export (cohérence des bases),
# redémarrés immédiatement après (double-run G4). Noms = inventaire T1.1-a
# (conteneurs-xpenology.csv) — à ajuster si le nom réel diffère.
declare -A XP_CONTAINERS=(
[vaultwarden]="vaultwarden-server-1"
[nextcloud]="nextcloud_app nextcloud_db"
[photoprism]="photoprism photoprism_db"
[gitea]="gitea"
[n8n]="n8n"
)
# Photoprism lit les originaux IN-PLACE (pas de migration) : /data/famille/photos doit
# déjà être en place sur le G9 (T2.2/T2.4). Seuls storage + db sont migrés.
if [[ "$BRIQUE" == "photoprism" ]]; then
log "⚠️ PhotoPrism : les originaux sont lus IN-PLACE sur /data/famille/photos (non migrés)."
log " Vérifiez que /data/famille/photos est bien en place sur le G9 avant 'compose up'."
fi
XP_MAP_LINE="${XP_MAP[$BRIQUE]:-}"
[[ -n "$XP_MAP_LINE" ]] || { echo "ERREUR : mapping inconnu pour $BRIQUE (compléter XP_MAP)"; exit 1; }
# Décomposer le mapping en paires (xp_dir, g9_subdir)
IFS=';' read -ra PAIRS <<< "$XP_MAP_LINE"
XP_DIRS=()
G9_SUBS=()
for pair in "${PAIRS[@]}"; do
[[ -n "$pair" ]] || continue
XP_DIRS+=("${pair%%:*}")
G9_SUBS+=("${pair##*:}")
done
log "=== ÉTAPE 1/3 : VÉRIFICATION DES CHEMINS SOURCES (xpenology, lecture seule) ==="
MISSING=0
for i in "${!XP_DIRS[@]}"; do
if $XP_SSH "test -d '${XP_DIRS[$i]}'" 2>/dev/null; then
log " ✅ source présente : ${XP_DIRS[$i]} → compose/$BRIQUE/${G9_SUBS[$i]}/"
else
log " ❌ source ABSENTE : ${XP_DIRS[$i]} (attendu → compose/$BRIQUE/${G9_SUBS[$i]}/)"
MISSING=1
fi
done
if [[ $MISSING -eq 1 ]]; then
echo
echo "ERREUR : au moins un chemin source xpenology est absent."
echo "Les chemins de XP_MAP sont des hypothèses (conteneurs créés via GUI DSM)."
echo "Vérifiez l'arborescence réelle sur le xpenology (inventaire T1.2) et corrigez XP_MAP."
exit 1
fi
log "=== ÉTAPE 2/3 : TRANSFERT (rsync, chiffré via SSH) + INTÉGRITÉ fichier par fichier ==="
# --- Stop de la source (cohérence des bases vivantes) ---
STOPPED=()
for c in ${XP_CONTAINERS[$BRIQUE]:-}; do
if $XP_SSH "sudo $XP_DOCKER ps -q -f name=^${c}\$" 2>/dev/null | grep -q .; then
if $XP_SSH "sudo $XP_DOCKER stop $c" >/dev/null 2>&1; then
STOPPED+=("$c")
log " ⏸ source stoppée : $c"
else
log " ⚠️ échec du stop de $c — export potentiellement incohérent, À VÉRIFIER."
fi
else
log " ⚠️ conteneur '$c' non trouvé running sur .25 — vérifier l'inventaire (cohérence non garantie)."
fi
done
start_source() {
for c in "${STOPPED[@]:-}"; do
$XP_SSH "sudo $XP_DOCKER start $c" >/dev/null 2>&1 && log " ▶ source redémarrée : $c" || log " ⚠️ ÉCHEC du redémarrage de $c — INTERVENIR SUR .25."
done
}
trap start_source EXIT
TOTAL_FILES=0
for i in "${!XP_DIRS[@]}"; do
XP_DIR="${XP_DIRS[$i]}"
G9_SUB="${G9_SUBS[$i]}"
G9_DIR="$COMPOSE_DIR/$G9_SUB"
mkdir -p "$G9_DIR"
log "Transfert : $XP_DIR/ → $G9_DIR/"
rsync -a --info=progress2 -e "ssh -p $XP_PORT -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600" "$XP_USER@$XP_HOST:$XP_DIR/" "$G9_DIR/"
# Manifeste sha256 trié, chemins RELATIFS (identiques des deux côtés) → diff fiable.
log "Intégrité : manifeste sha256 (chemins relatifs, triés) pour $G9_SUB"
REMOTE_MANIFEST=$(mktemp)
LOCAL_MANIFEST=$(mktemp)
$XP_SSH "cd '$XP_DIR' && find . -type f -exec sha256sum {} + 2>/dev/null | sort" > "$REMOTE_MANIFEST"
( cd "$G9_DIR" && find . -type f -exec sha256sum {} + 2>/dev/null | sort ) > "$LOCAL_MANIFEST"
R_COUNT=$(wc -l < "$REMOTE_MANIFEST")
L_COUNT=$(wc -l < "$LOCAL_MANIFEST")
TOTAL_FILES=$(( TOTAL_FILES + R_COUNT ))
log " fichiers : source=$R_COUNT local=$L_COUNT"
if diff -u "$REMOTE_MANIFEST" "$LOCAL_MANIFEST" > "${REMOTE_MANIFEST}.diff"; then
log " ✅ intégrité OK ($G9_SUB)"
else
log " ❌ ÉCHEC intégrité ($G9_SUB) — différences :"
head -n 50 "${REMOTE_MANIFEST}.diff"
rm -f "$REMOTE_MANIFEST" "$LOCAL_MANIFEST" "${REMOTE_MANIFEST}.diff"
journal "import ÉCHEC intégrité sur $G9_SUB" "Vérification G4 (intégrité)" "manifeste sha256 différent" "supprimer le sous-dossier importé, re-transférer"
echo "ERREUR : l'intégrité a échoué sur $G9_SUB. Ne pas lancer le service. Re-vérifier le transfert."
exit 1
fi
rm -f "$REMOTE_MANIFEST" "$LOCAL_MANIFEST" "${REMOTE_MANIFEST}.diff"
done
log "Intégrité globale OK : $TOTAL_FILES fichiers vérifiés."
journal "export+import OK" "Migration G4 (copie, xpenology non modifié)" "$TOTAL_FILES fichiers, manifeste sha256 identique" "suppression du conteneur + sous-dossiers importés"
# --- Redémarrage de la source AVANT de toucher au G9 (double-run G4) ---
start_source
log "=== ÉTAPE 3/3 : LANCEMENT DU SERVICE SUR LE G9 ==="
log "docker compose up -d dans $COMPOSE_DIR"
docker compose -f "$COMPOSE_DIR/docker-compose.yml" up -d
log "Vérification du service (healthcheck)"
sleep 10
docker compose -f "$COMPOSE_DIR/docker-compose.yml" ps
log "Terminé. VALIDER MANUELLEMENT le service (login, 1 fichier, 1 recherche) AVANT de passer à la brique suivante."
journal "service lancé sur le G9" "Double-run G4" "compose up -d, healthcheck à valider" "docker compose down + suppression des données importées"