219 lines
10 KiB
Bash
Executable File
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"
|