From 34e0d470c1e934f5b318c8ba82fb5e790fbd5909 Mon Sep 17 00:00:00 2001 From: Effi Date: Sun, 11 Oct 2026 13:04:34 +0200 Subject: [PATCH] =?UTF-8?q?T4.2=20Nextcloud=20migr=C3=A9=20(07c)=20+=20Col?= =?UTF-8?q?labora:=20--o:ssl.enable=3Dfalse=20(TLS=20image=20par=20d=C3=A9?= =?UTF-8?q?faut)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GUIDE-MATRIX.md | 215 +++++++++++++++++ g9-migration/PHASE4-RECOLLEMENT.md | 75 ++++++ g9-migration/README.md | 13 +- g9-migration/compose/n8n/docker-compose.yml | 50 ++-- .../compose/nextcloud/docker-compose.yml | 85 +++++-- .../compose/photoprism/docker-compose.yml | 59 +++-- g9-migration/journal.md | 16 ++ g9-migration/scripts/03b-secrets.sh | 32 ++- g9-migration/scripts/07-migrate-stateful.sh | 9 + g9-migration/scripts/07c-migrate-nextcloud.sh | 223 ++++++++++++++++++ .../scripts/07d-migrate-photoprism.sh | 128 ++++++++++ 11 files changed, 851 insertions(+), 54 deletions(-) create mode 100644 GUIDE-MATRIX.md create mode 100644 g9-migration/PHASE4-RECOLLEMENT.md create mode 100755 g9-migration/scripts/07c-migrate-nextcloud.sh create mode 100755 g9-migration/scripts/07d-migrate-photoprism.sh diff --git a/GUIDE-MATRIX.md b/GUIDE-MATRIX.md new file mode 100644 index 0000000..036447b --- /dev/null +++ b/GUIDE-MATRIX.md @@ -0,0 +1,215 @@ +# Guide Matrix — utilisation et connexion + +> **Objet** : rejoindre et administrer le serveur Matrix (Synapse + Element) auto-hébergé. +> **Source** : déploiement documenté dans `g9-migration/journal.md` (Phase 2, 10/10/2026) et `DOSSIER-SOFTWARE.md` S8. +> **Aucun secret dans ce guide** — les identifiants vivent dans **Vaultwarden**, jamais dans le dépôt (`.gitignore` : `matrix.env`, `*.key`, `*.cred`). + +--- + +## 1. État du serveur + +| Composant | Machine | URL | Rôle | +|-----------|---------|-----|------| +| **Synapse** (homeserver) | G9 (`.24`) | `http://192.168.1.24:8008` | Serveur Matrix (API + fédération) | +| **Element Web** (client) | G9 (`.24`) | `http://192.168.1.24:8009` | Client web, préconfiguré sur le homeserver local | +| **Salon ops** | — | `!mp7MFx9QbrJ0ARtzw…` | Canal d'alertes (watchtower, restic, n8n) — user `ops-bot` | +| **Cible TLS (Phase 4)** | — | `https://matrix.folzocorp.fr` | Accès sécurisé via Caddy (planifié, non en place) | + +- Version Synapse : **1.162.0** ; version Element Web : **1.12.30**. +- `server_name` du homeserver : `192.168.1.24` → les identifiants ont la forme `@pseudo:192.168.1.24`. +- Cible finale documentée : migration Matrix + Jitsi vers le **J5040 (`.1`)** (16 Go) — voir `DOSSIER-SOFTWARE.md` S2.4/S8. + +--- + +## 2. Connexion + +### 2.1 Element Web (PC, LAN) + +1. Ouvrir **`http://192.168.1.24:8009`**. +2. Le homeserver est **déjà préconfiguré** (`config.json` monté en lecture seule dans le conteneur → `base_url: http://192.168.1.24:8008`). Rien à saisir. +3. Se connecter avec son compte (§3). + +> ⚠️ **En `http://IP`, Element Web affiche un rond infini** (contexte non sécurisé) — lire la section **5. Dépannage** avant de diagnostiquer le serveur. Fix recommandé : tunnel `localhost` (§5.2) ou TLS Phase 4. + +### 2.2 Element Android (téléphone) + +1. Installer **Element** (Play Store / F-Droid). +2. À l'écran de bienvenue : **Modifier le serveur d'accueil** (Changer de serveur). +3. Saisir `http://192.168.1.24:8008` → **Suivant** → **Se connecter**. +4. Nom d'utilisateur = le pseudo **sans le domaine** (ex. `effi`, pas `@effi:192.168.1.24`) + mot de passe. + +### 2.3 Element Desktop (PC) + +1. Installer Element Desktop (le navigateur n'est pas concerné par la restriction de contexte sécurisé). +2. Écran de connexion → **Modifier le serveur d'accueil** → `http://192.168.1.24:8008`. + +### 2.4 Accès depuis l'extérieur (hors LAN) + +Matrix n'est **pas exposé** sur Internet (volontairement). Deux voies : + +| Voie | Comment | +|------|---------| +| **WireGuard** (J5040 `.1`) | Se connecter au VPN depuis le téléphone/PC portable → le LAN `.1/24` est atteignable → ouvrir `http://192.168.1.24:8009` (Android : saisir directement le homeserver dans l'app) | +| **TLS Phase 4** (à faire) | Caddy + `matrix.folzocorp.fr` → l'app Element utilise `https://matrix.folzocorp.fr`, accessible de façon sécurisée | + +--- + +## 3. Création de compte + +> L'**inscription publique est désactivée** (pas de self-registration sur un serveur privé). Les comptes se créent **côté serveur**, en SSH sur le G9 (`.24`). + +### 3.1 Prérequis : un compte admin Synapse + +Si aucun admin n'existe, en créer un via le conteneur Synapse : + +```bash +# sur .24 — trouver le nom du conteneur +docker ps --format '{{.Names}}' | grep -i synapse + +# créer un utilisateur admin (le script lit le shared secret du homeserver.yaml) +docker exec -it register_new_matrix_user \ + -c /data/homeserver.yaml -a -u -p '' +``` + +> Si `register_new_matrix_user` refuse (pas de `registration_shared_secret` dans `homeserver.yaml`) : l'ajouter via `sudo` dans `/data/homeserver.yaml` (`registration_shared_secret: `, généré par `openssl rand -hex 32`), **restart du conteneur**, relancer la commande, puis retirer le secret et restart à nouveau. Même pattern que `room_creation: true` (journal 2026-10-09). Stocker `` dans **Vaultwarden**. + +### 3.2 Créer les comptes (famille, bots) via l'API admin + +**Étape 1 — obtenir un token admin** (login avec le compte admin) : + +```bash +curl -s -X POST 'http://192.168.1.24:8008/_matrix/client/v3/login' \ + -H 'Content-Type: application/json' \ + -d '{ + "auth": { + "type": "m.login.password", + "identifier": { "type": "m.id.user", "user": "" }, + "passphrase": "" + } + }' +# → réponse JSON : récupérer "access_token" (= ) +``` + +**Étape 2 — créer l'utilisateur** (API admin v2, `PUT` = enregistrement) : + +```bash +curl -s -X PUT 'http://192.168.1.24:8008/_synapse/admin/v2/users/@:192.168.1.24' \ + -H "Authorization: Bearer " \ + -H 'Content-Type: application/json' \ + -d '{ + "password": "", + "display_name": "", + "admin": false + }' +# → 201 Created = compte créé +``` + +- `admin: true` pour un second administrateur, `false` pour la famille/bots. +- Le pseudo est en minuscules, sans espaces ni accents (règle Matrix). + +### 3.3 Première connexion + chiffrement (E2E) + +1. Ouvrir Element (§2), se connecter avec `@:192.168.1.24` (Web/Desktop : pseudo seul suffit). +2. Element demande une **phrase de passe de récupération** (secure backup) → la **noter hors-ligne** (papier), c'est la porte de sortie du chiffrement. +3. Vérifier ses sessions : **Paramètres → Sécurité → Vérification** (QR code entre téléphone et PC) pour valider le chiffrement de bout en bout. + +### 3.4 Désinscription / suppression + +- **Désactiver** un compte (propre, conserve la trace) : + ```bash + curl -s -X POST 'http://192.168.1.24:8008/_synapse/admin/v1/deactivate/' \ + -H "Authorization: Bearer " \ + -H 'Content-Type: application/json' \ + -d '{ "erase": true }' + ``` +- Vérifier que l'inscription publique reste fermée dans `homeserver.yaml` : `disable_registration: true` (ne pas activer `enable_registration` sans point d'accès protégé). + +--- + +## 4. Usage type (rappel du plan, S8) + +| Salon | Contenu | +|-------|---------| +| **Famille** | Messagerie quotidienne (remplace WhatsApp), visio Jitsi intégrée à Element | +| **Ops** | Alertes machine : watchtower (MAJ images), restic (backup G9), n8n — via `ops-bot` | +| **hermes** (prévu) | Salon où la famille parle à Effi (assistant, Phase 3 du second cerveau) | + +--- + +## 5. Dépannage + +### 5.1 Le rond infini dans Element Web (cause racine documentée) + +**Symptôme** : page noire, spinner infini sur `http://192.168.1.24:8009`. Console (F12) : + +``` +Browser missing feature: 'crypto' +Browser missing feature: 'securecontext' +Error initialising Matrix session TypeError: window.crypto.randomUUID is not a function +``` + +**Cause** : Element ≥ 1.12 exige un **contexte sécurisé** (HTTPS, `localhost` ou `file:`). En `http://`, le navigateur coupe `crypto.randomUUID` → Element n'arrive pas à initialiser la session. **Le serveur n'y est pas pour rien.** + +Anodins (à ne pas poursuivre) : `404 config..json` (config optionnelle par hôte) et `404 /_matrix/client/v1/auth_metadata` (endpoint optionnel). + +**Cause secondaire** : un vieux **service worker** peut garder en cache une config obsolète (la config cassée « matrix.org » d'avant le fix du 10/10). Nettoyage : F12 → Application → Service Workers → *Unregister*, puis Storage → *Clear site data*, puis Ctrl+Maj+R. + +### 5.2 Workaround sans TLS : tunnel localhost + +`localhost` est un contexte sécurisé → Element fonctionne. Depuis le PC : + +```bash +ssh -L 8009:192.168.1.24:8009 -L 8008:192.168.1.24:8008 @192.168.1.24 +# puis ouvrir http://localhost:8009 +``` + +### 5.3 Fix définitif : TLS (Phase 4) + +Caddy + `matrix.folzocorp.fr` (même pattern que `obsidian.folzocorp.fr`, journal Phase 4) → Element Web servi en HTTPS, spinner résolu, accès distant propre. + +### 5.4 Commandes de diagnostic (côté serveur, tout doit répondre) + +```bash +# 1) API vivante → JSON avec "versions":[...] (200) +curl -s http://192.168.1.24:8008/_matrix/client/versions + +# 2) Element servi + config correcte → 200 + base_url = http://192.168.1.24:8008 +curl -s http://192.168.1.24:8009/ -o /dev/null -w '%{http_code}\n' +curl -s http://192.168.1.24:8009/config.json + +# 3) CORS (sans lui, Element web est muet) → Access-Control-Allow-Origin: * +curl -s -D - -o /dev/null -H "Origin: http://192.168.1.24:8009" \ + http://192.168.1.24:8008/_matrix/client/versions | grep -i access-control + +# 4) Conteneurs +docker ps --format '{{.Names}}\t{{.Status}}' | grep -Ei 'synapse|element' +``` + +**Résultats vérifiés le 11/10/2026** : 1) `200` + versions r0.0.1→v1.12 ; 2) `200` + `base_url: http://192.168.1.24:8008` ; 3) `Access-Control-Allow-Origin: *` ; 4) synapse `Synapse/1.162.0` UP. Si ces 4 tests passent et qu'Element tourne en rond → problème 100 % navigateur (§5.1/5.2). + +### 5.5 Alertes ops absentes du salon ops + +- Vérifier le token `ops-bot` : côté G9, `/etc/restic/matrix.env` (root, `600`) pour restic ; workflow n8n « Watchtower Notification » pour les MAJ d'images. +- Test d'envoi direct : + ```bash + curl -s -X POST 'http://192.168.1.24:8008/_matrix/client/v3/rooms/!mp7MFx9QbrJ0ARtzw…/send/m.room.message/test1' \ + -H "Authorization: Bearer " \ + -H 'Content-Type: application/json' \ + -d '{"msgtype":"m.text","body":"test alerte"}' + # → 200 + {"event_id":"$…"} + ``` + +--- + +## 6. Sécurité (règles du projet) + +| Règle | Application Matrix | +|-------|--------------------| +| Secrets hors dépôt | Token admin, mot de passe admin, `matrix.env` → **Vaultwarden** / root only, jamais git | +| Pas d'exposition Internet | Accès LAN ou WireGuard ; TLS Phase 4 pour l'exposition contrôlée | +| 2FA | À activer sur les comptes admin (S9 : 2FA sur tous les services exposés) | +| Comptes famille | `admin: false`, mot de passe unique par personne, phrase de récupération E2E notée hors-ligne | + +--- + +*Guide créé le 11/10/2026 — état vérifié ce jour sur `.24` (Synapse 1.162.0, Element 1.12.30). À réaligner quand Matrix migrera vers le J5040 (`.1`) et que `matrix.folzocorp.fr` (Caddy TLS) sera en place.* diff --git a/g9-migration/PHASE4-RECOLLEMENT.md b/g9-migration/PHASE4-RECOLLEMENT.md new file mode 100644 index 0000000..2fe1fde --- /dev/null +++ b/g9-migration/PHASE4-RECOLLEMENT.md @@ -0,0 +1,75 @@ +# Phase 4 — récupération du thread supprimé + +**Session** : `e67f1166-d70c-42cd-a295-79513757233e` (tâche Kanban `8e3e7f073a6806cd` — PHASE 4) +**Suppression** : 2026-10-11 07:20:04 UTC (trace systemd : `Runner error FOREIGN KEY constraint failed, sessionId=e67f1166…`) +**Méthode** : suppression = `DELETE` sur `sessions` + cascade sur `events`. Les pages SQLite libérées gardent leur contenu tant qu'elles ne sont pas réallouées → caravage des pages libres de `sessions.db` (snapshot), reconstruction des enregistrements. + +## 1. Ce qui a été récupéré + +| Élément | Valeur | +|---|---| +| Snapshot de travail | `snap1` pris à 08:20:34 UTC (1 h après la suppression) | +| Pages contenant l'id de session | 21 232 | +| Texte imprimable ≥ 400 caractères | 34 227 blocs = **47,9 Mo** | +| Objets JSON payload reconstitués | **51 420 = 34,68 Mo** | +| Entrées d'index `events` (session_id, seq / event_type) | 79 830 → 37 821 seq, 42 009 event_type, plage seq 5 → 158 799 | +| Tours de conversation reconstitués | **130** (129 assistant + 3 user côté rôles) | +| Événements réinsérés dans `sessions.db` | **64 657** (33,85 Mo de payload) | +| Appels d'outils avec arguments intacts | 31 998 | +| Résultats / sorties d'outils | 124 / 275 | + +## 2. Ce qui est perdu (non inventé) + +- **~75 % des événements** : les pages leaf/overflow ont été réallouées et **zérées** par SQLite après la suppression (15 951 pages contenaient encore l'id de session mais avec leur tableau de cellules remis à zéro). +- **La prose du fil** (messages de l'assistant) est récupérée **en fragments dans le désordre** : les deltas de streaming sont stockés dans des pages distinctes, l'ordre d'origine n'a pas pu être reconstitué. Les 130 messages insérés contiennent donc des fragments réels, recollés dans l'ordre des pages (ordre d'allocation ≈ ordre chronologique), pas un texte propre. +- **Presque tous les messages utilisateur** sont perdus (payloads courts, pages détruites) : 3 rôles `user` identifiés, sans contenu. +- Volume attendu ≈ 158 800 événements / 80–110 Mo de payload → **≈ 35 % récupéré**. + +## 3. Où sont les artefacts + +| Fichier | Contenu | +|---|---| +| `/home/folzo/openfox/_recover/snap1/` | copie octet-pour-octet d'origine (`sessions.db`, `-wal`, `-shm`) | +| `/home/folzo/openfox/_recover/orig.sha256` | sha256 des fichiers d'origine | +| `/home/folzo/openfox/_recover/snap2/` + `snap2.sha256` | état **immédiatement avant** la réinsertion (point de rollback) | +| `/home/folzo/openfox/_recover/phase4_json.jsonl` | 51 420 payloads JSON bruts (+ page, offset) | +| `/home/folzo/openfox/_recover/phase4_index.jsonl` | 79 830 entrées d'index (rowid, seq, event_type) | +| `/home/folzo/openfox/_recover/phase4_events.jsonl` | flux d'événements synthétisé | +| `g9-migration/PHASE4-RECOLLEMENT.md` §5 | journal des actions récupérées | + +## 4. Rollback + +**Chirurgical** (recommandé, ne touche à rien d'autre) : + +```sql +DELETE FROM events WHERE session_id='e67f1166-d70c-42cd-a295-79513757233e'; +DELETE FROM sessions WHERE id='e67f1166-d70c-42cd-a295-79513757233e'; +``` + +**Restauration à l'état pré-réinsertion** : + +``` +systemctl stop openfox +cp /home/folzo/openfox/_recover/snap2/sessions.db{,-wal,-shm} /home/folzo/.local/share/openfox/ +sha256sum -c /home/folzo/openfox/_recover/snap2.sha256 +systemctl start openfox +``` + +`PRAGMA integrity_check` = `ok` avant et après réinsertion. Les fichiers d'origine n'ont jamais été modifiés en écriture directe : la réinsertion est passée par SQLite (WAL), et `snap1` reste la copie de l'état post-suppression. + +## 5. Journal des actions récupérées + +Le journal des 32 409 actions récupérées (appels d'outils + arguments intacts, résultats, +sorties) n'est **pas versionné ici** — il duplique les artefacts et concerne la base de +sessions OpenFox, pas le kit G9. Consultable / rejouable : + +| Artefact | Contenu | +|---|---| +| `/home/folzo/openfox/_recover/phase4_actions.md` | journal brut complet (32 409 lignes, page-ordre) | +| `/home/folzo/openfox/_recover/phase4_actions_curated.md` | version filtrée/dédupliquée (758 lignes) | +| `/home/folzo/openfox/_recover/phase4_json.jsonl` | 51 420 payloads JSON (+ page, offset) | +| `/home/folzo/openfox/_recover/phase4_events.jsonl` | flux d'événements réinsérés dans la base | + +Reprise du travail Phase 4 : la tâche Kanban `8e3e7f073a6806cd` est toujours bindée à la +session `e67f1166…` et le runner n'a **jamais cessé d'écrire** (journal 09:18 → 10:13, +commits `a1864c6`, scripts 07c/07d) — c'est la source operative, pas le fil recoveré. diff --git a/g9-migration/README.md b/g9-migration/README.md index f545d97..9ad3493 100644 --- a/g9-migration/README.md +++ b/g9-migration/README.md @@ -29,7 +29,7 @@ xpenology → G9, phase par phase, quand le G9 sera branché (IP temporaire `.24 | G1 | Scripts idempotents, en-têtes documentant chaque modification. | | G2 | `06-restic-backup.sh` : backup + **test de restauration** avant toute migration. | | G3 | `06b-usb-media.sh`, `08-decommission.sh` : drapeau `--i-understand` + saisie du nom du disque / `WIPE XPENOLOGY`. | -| G4 | `07-migrate-stateful.sh` : lecture seule du xpenology + intégrité sha256 ; `08-decommission.sh` exige la validation de toutes les briques avant wipe. | +| G4 | `07a`–`07d` (+ `07-migrate-stateful.sh` générique) : lecture seule du xpenology + intégrité sha256 ; `08-decommission.sh` exige la validation de toutes les briques avant wipe. | | G5 | README (ordre d'exécution) + `08-decommission.sh` : IP `.24`→`.25` uniquement au décommissionnement. | | G6 | `01-hardening.sh` : ufw LAN-only (`192.168.1.0/24`), pas d'exposition WAN. | | G7 | Chaque script : section **Rollback** en en-tête + colonne Rollback du journal. | @@ -52,7 +52,14 @@ g9-migration/ │ ├── 05-beszel.sh ← T2.5 : agent Beszel │ ├── 06-restic-backup.sh ← T3.1 : restic G9 → Freebox + test restauration │ ├── 06b-usb-media.sh ← T6.1 : montage des disques USB média → /mnt/usb-media -│ ├── 07-migrate-stateful.sh← T4.x : export/import des stateful (xpenology → G9) +│ ├── 06c-postboot-check.sh ← contrôles post-reboot (lecture seule, 15+ checks) +│ ├── 07-migrate-stateful.sh← T4.x : générique (rsync) — INOPÉRANT sur le DSM : sshd +│ │ refuse `rsync --server` et scp → utiliser 07a–07d +│ ├── 07a-migrate-gitea.sh ← T4.4 : Gitea + backend Postgres (transport tar sur SSH) +│ ├── 07b-migrate-vaultwarden.sh ← T4.1 : Vaultwarden (coffre + 2FA + ADMIN_TOKEN) +│ ├── 07c-migrate-nextcloud.sh ← T4.2 : Nextcloud + MariaDB + Collabora +│ ├── 07d-migrate-photoprism.sh ← T4.3 : PhotoPrism (copie 530 Go + indexation) +│ ├── restic-notify.sh ← alerte Matrix des jobs restic (posée par 06) │ └── 08-decommission.sh ← T6.4 : décommissionnement xpenology [DESTRUCTIF] ├── compose/ ← docker-compose.yml versionnés (stockés aussi dans Gitea) │ ├── vaultwarden/ ← T4.1 @@ -74,7 +81,7 @@ g9-migration/ | 1 | (xpenology) exclure stateful de Watchtower + inventaire | G4 | | 2 | `01-hardening.sh` → `02-data.sh` → `03-docker.sh` → `03b-secrets.sh` → `04-smb.sh` → `05-beszel.sh` | — | | 3 | `06-restic-backup.sh` (backup + test restauration) | G2 | -| 4 | `07-migrate-stateful.sh` (par brique, double-run) — **après** `03b-secrets.sh` | G4 | +| 4 | **par brique, dans l'ordre** — **après** `03b-secrets.sh` : `07b-migrate-vaultwarden.sh` (T4.1) → `07c-migrate-nextcloud.sh` (T4.2) → `07d-migrate-photoprism.sh` (T4.3) → `07a-migrate-gitea.sh` (T4.4) → n8n (T4.5) → DNS/TLS (T4.6). `07-migrate-stateful.sh` = script **générique rsync**, inopérant sur le DSM (sshd refuse `rsync --server`/scp) ; transport réel des 07a–07d = **tar sur SSH** | G4 | | 5 | restic 3-2-1 complet (G9 + PC IA → Freebox → 218play) | G2 | | 6 | `06b-usb-media.sh` (T6.1) → Jellyfin (T6.2) → IP .24→.25 → `08-decommission.sh` | G3, G4, G5 | | 7 | (G7) wipe DSM + Ubuntu neuf + boot test | G3, G10 | diff --git a/g9-migration/compose/n8n/docker-compose.yml b/g9-migration/compose/n8n/docker-compose.yml index cd685c2..f82938e 100644 --- a/g9-migration/compose/n8n/docker-compose.yml +++ b/g9-migration/compose/n8n/docker-compose.yml @@ -1,44 +1,62 @@ -# n8n — T4.5 (automatisation + alertes → Matrix, G9) -# Le conteneur n8n est stateless (image) mais sa DB (Postgres) est stateful → exclu. -# Workflows exportables (JSON). Licence fair-code (notée, S3.1). +# n8n — T4.5 (automatisation, G9). Source .25 = n8n **2.42.5** sur Postgres 17 +# (db `2025_n8n`, user `2025_n8n_user`, 4 workflows, 4 credentials, 1 user) → vérité terrain. +# Image **épinglée à 2.42.5** : pas de migration de schéma pendant le double-run (S11.4). +# Clé de chiffrement : .n8n/config importé depuis la source (copie du volume `data`) → les +# credentials existants restent déchiffrables. n8n ne connaît pas les *_FILE → env_file. services: n8n: - image: n8nio/n8n:1.90 + image: docker.n8n.io/n8nio/n8n:2.42.5 container_name: n8n restart: unless-stopped labels: - - "com.centurylinklabs.watchtower.enable=true" # image stateless → auto + - "com.centurylinklabs.watchtower.enable=false" # version épinglée (schéma DB) → exclu + env_file: + - ./secrets/n8n.env environment: - TZ=Europe/Paris - N8N_HOST=n8n.folzocorp.fr - N8N_PROTOCOL=https - N8N_PORT=5678 - - DB_TYPE=postgresdb - - DB_POSTGRESDB_HOST=n8n_db - - DB_POSTGRESDB_DATABASE=n8n - - DB_POSTGRESDB_USER=n8n - - DB_POSTGRESDB_PASSWORD_FILE=/run/secrets/dbpass + - WEBHOOK_URL=https://n8n.folzocorp.fr + - N8N_RELEASE_TYPE=stable + - N8N_RUNNERS_ENABLED=true + - NODE_ENV=production - GENERIC_TIMEZONE=Europe/Paris + - DB_TYPE=postgresdb + - DB_POSTGRESDB_HOST=db + - DB_POSTGRESDB_PORT=5432 + - DB_POSTGRESDB_DATABASE=2025_n8n + - DB_POSTGRESDB_USER=2025_n8n_user + # Double-run LAN : N8N_SECURE_COOKIE=false permet l'accès http://192.168.1.24:5678 + # (cookies « secure » refusés en http). **À remettre à true à la bascule TLS (T4.6).** + - N8N_SECURE_COOKIE=false volumes: - ./data:/home/node/.n8n - ./secrets:/run/secrets:ro - # Bind 0.0.0.0 : accessible depuis le LAN (J5040 edge/Caddy le proxy). - # ufw (01-hardening.sh) restreint le port au LAN 192.168.1.0/24 → pas d'exposition WAN. + # Bind 0.0.0.0 : LAN uniquement (ufw de 01-hardening.sh restreint le port). ports: - "0.0.0.0:5678:5678" depends_on: - - n8n_db + db: + condition: service_healthy - n8n_db: + db: image: postgres:17 container_name: n8n_db restart: unless-stopped labels: - "com.centurylinklabs.watchtower.enable=false" # DB stateful → exclu environment: - - POSTGRES_DB=n8n - - POSTGRES_USER=n8n + - POSTGRES_DB=2025_n8n + - POSTGRES_USER=2025_n8n_user - POSTGRES_PASSWORD_FILE=/run/secrets/dbpass + - POSTGRES_INITDB_ARGS=--encoding=UTF8 --locale=C volumes: - ./db:/var/lib/postgresql/data - ./secrets:/run/secrets:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U 2025_n8n_user -d 2025_n8n"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s diff --git a/g9-migration/compose/nextcloud/docker-compose.yml b/g9-migration/compose/nextcloud/docker-compose.yml index a4dcb92..44acd06 100644 --- a/g9-migration/compose/nextcloud/docker-compose.yml +++ b/g9-migration/compose/nextcloud/docker-compose.yml @@ -1,43 +1,59 @@ # Nextcloud — T4.2 (socle famille, G9) -# Stateful (app + MariaDB + Redis) → exclus de Watchtower (S11.4). +# Stateful (app + MariaDB + Redis + données) → exclus de Watchtower (S11.4). # MAJ manuelle supervisée (S11.5) : backup DB → maintenance ON → pull/up → occ upgrade → vérif → OFF. # L'incident Nextcloud (06/10) est la raison de ce pinning + exclusion. +# +# CONTRAINTES MESURÉES sur la source .25 (2026-10-11) : +# - config.php : dbtype=mysql, dbhost=**db**, dbname=nextcloud, dbuser=nextcloud, +# datadirectory=/var/www/html/data, version 35.0.1.1, instanceid oc9yed0p9k7q. +# → le service DB du compose G9 DOIT s'appeler `db` et porter les **identifiants +# d'origine** (sinon config.php importé ne correspond plus à la base importée). +# - apps déjà installés sur la source : calendar 6.6.2, contacts 8.9.1, mail 5.12.3 +# (Groupware), deck 1.19.0, notes 6.1.0, richdocuments 12.0.1, collectives, forms, +# spreed, whiteboard… → import = conservation à l'identique, rien à installer. +# - pas de chiffrement des fichiers activé (aucune config encryption) → copie simple. +# - éditeur : Collabora (décision Phase 4) → service `collabora` + richdocuments.wopi_url. services: nextcloud: - image: nextcloud:35 + image: nextcloud:35.0.1 # = version source exacte (35.0.1.1) → aucune migration de schéma container_name: nextcloud restart: unless-stopped labels: - "com.centurylinklabs.watchtower.enable=false" environment: - TZ=Europe/Paris - - POSTGRES_HOST=nextcloud_db - - POSTGRES_DB=nextcloud - - POSTGRES_USER=nextcloud - - POSTGRES_PASSWORD_FILE=/run/secrets/dbpass - - NEXTCLOUD_TRUSTED_PROXIES=172.16.0.0/12 + - PHP_MEMORY_LIMIT=1G + - PHP_UPLOAD_LIMIT=10G + - PHP_POST_SIZE_LIMIT=10G volumes: - ./data:/var/www/html - ./config:/var/www/html/config - ./secrets:/run/secrets:ro - depends_on: - - nextcloud_db - - redis - # Bind 0.0.0.0 : accessible depuis le LAN (J5040 edge/Caddy le proxy). - # ufw (01-hardening.sh) restreint le port au LAN 192.168.1.0/24 → pas d'exposition WAN. + # Bind 0.0.0.0 : accessible depuis le LAN (edge/Caddy le proxy en T4.6). ports: - "0.0.0.0:8080:80" + depends_on: + - db + - redis + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost/status.php"] + interval: 60s + timeout: 10s + retries: 5 - nextcloud_db: + db: image: mariadb:10.11 container_name: nextcloud_db restart: unless-stopped labels: - "com.centurylinklabs.watchtower.enable=false" # DB stateful → exclu + command: + - --transaction-isolation=READ-COMMITTED + - --binlog-format=ROW environment: - MARIADB_DATABASE=nextcloud - MARIADB_USER=nextcloud - - MARIADB_PASSWORD_FILE=/run/secrets/dbpass + - MARIADB_PASSWORD_FILE=/run/secrets/dbpass # = mot de passe D'ORIGINE (import) - MARIADB_ROOT_PASSWORD_FILE=/run/secrets/rootpass volumes: - ./db:/var/lib/mysql @@ -48,6 +64,45 @@ services: container_name: nextcloud_redis restart: unless-stopped labels: - - "com.centurylinklabs.watchtower.enable=false" # cache persistant → exclu + - "com.centurylinklabs.watchtower.enable=false" volumes: - ./redis:/data + + # Collabora CODE — éditeur de documents (décision Phase 4, remplace OnlyOffice .25). + # Image épinglée 26.04.5.1.1 → Watchtower OFF (convention S11.4 : épinglée = exclu, + # cf. label n8n). MAJ manuelle supervisée avec backup préalable. + collabora: + image: collabora/code:26.04.5.1.1 + container_name: collabora + restart: unless-stopped + labels: + - "com.centurylinklabs.watchtower.enable=false" + # L'image officielle ne substitue que `username`/`password` dans coolwsd.xml : + # `password_file` n'est PAS lu → console 9980 restée sans mot de passe. Mot de passe + # injecté par env_file (pattern n8n) ; secrets/collabora.env généré par 03b-secrets.sh. + env_file: + - ./secrets/collabora.env + # Entrypoint = coolwsd (arguments fixes). CMD est **appendé** à l'entrypoint → + # `--o:ssl.enable=false` désactive le TLS interne (mesuré 2026-10-11 : sans cela, + # discovery http = 000, https -k = 200 ; env O:ssl.enable NON appliqué). + command: + - --o:ssl.enable=false + environment: + - TZ=Europe/Paris + # Hôtes émetteurs de WOPI autorisés (double-run : LAN G9 + hostname de bascule T4.6). + - aliasgroup1=https://nextcloud.folzocorp.fr,http://192.168.1.24:8080,http://192.168.1.25:8080 + - username=admin + - JWT_ENABLED=false + # Image 26.04 : Entrypoint = coolwsd --use-env-vars → overrides coolwsd.xml via + # variables `O:` (mesuré : extra_features/EXTRA_FEATURES non lus, TLS par + # défaut). SSL interne désactivé : WOPI en http LAN ; Caddy (T4.6) terminera le TLS. + - O:ssl.enable=false + volumes: + - ./secrets:/run/secrets:ro + ports: + - "0.0.0.0:9980:9980" + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:9980/hosting/discovery"] + interval: 60s + timeout: 10s + retries: 5 diff --git a/g9-migration/compose/photoprism/docker-compose.yml b/g9-migration/compose/photoprism/docker-compose.yml index 47acd5a..2cc3200 100644 --- a/g9-migration/compose/photoprism/docker-compose.yml +++ b/g9-migration/compose/photoprism/docker-compose.yml @@ -1,6 +1,8 @@ -# PhotoPrism — T4.3 (photos, G9) -# Lecture IN-PLACE des originaux (S5) : pointe sur /data/famille/photos (pas de déplacement). -# Stateful (base + index) → exclu de Watchtower (S11.4). Image PINNÉE (S11.4, pas :latest). +# PhotoPrism — T4.3 (G9) +# Décision #3 (C-T4-DECISIONS) : les photos sont **copiées** sur le G9 (/data/docker/photoprism/originals), +# plus de lecture in-place sur le NAS. Image épinglée (S11.4), stateful (base + index) → Watchtower OFF. +# PhotoPrism ne connaît PAS les variables « *_FILE » : les secrets sont fournis par env_file +# (secrets/photoprism.env, 600 root, hors git) — MariaDB, lui, accepte *_FILE. services: photoprism: image: photoprism/photoprism:260919 @@ -8,34 +10,51 @@ services: restart: unless-stopped labels: - "com.centurylinklabs.watchtower.enable=false" + env_file: + - ./secrets/photoprism.env environment: - TZ=Europe/Paris - - PHOTOPRISM_URL=https://photos.folzocorp.fr - - PHOTOPRISM_SITE_URL=https://photos.folzocorp.fr - - PHOTOPRISM_ADMIN_USER=admin - - PHOTOPRISM_ADMIN_PASSWORD_FILE=/run/secrets/admin - - PHOTOPRISM_LIBRARY_PATH=/originals:/import + - PHOTOPRISM_SITE_URL=http://192.168.1.24:2342/ + - PHOTOPRISM_SITE_TITLE=Photos Folzo + - PHOTOPRISM_DATABASE_DRIVER=mysql + - PHOTOPRISM_DATABASE_SERVER=db:3306 + - PHOTOPRISM_DATABASE_NAME=photoprism - PHOTOPRISM_DATABASE_USER=photoprism - - PHOTOPRISM_DATABASE_PASSWORD_FILE=/run/secrets/dbpass - - PHOTOPRISM_LOCAL_NETWORKS=172.16.0.0/12,192.168.0.0/16 + - PHOTOPRISM_LOCAL_NETWORKS=192.168.0.0/16 + - PHOTOPRISM_HTTP_COMPRESSION=gzip + - PHOTOPRISM_LOG_LEVEL=info + - PHOTOPRISM_PUBLIC=false + - PHOTOPRISM_READONLY=false + - PHOTOPRISM_EXPERIMENTAL=false + - PHOTOPRISM_ORIGINALS_LIMIT=5000 + - PHOTOPRISM_AUTO_INDEX=300 + - PHOTOPRISM_AUTO_IMPORT=-1 - PHOTOPRISM_SIDECAR_ENABLED=true volumes: - - /data/famille/photos:/originals:ro # in-place (lecture seule) - - ./storage:/storage - - ./secrets:/run/secrets:ro - # Bind 0.0.0.0 : accessible depuis le LAN (J5040 edge/Caddy le proxy). - # ufw (01-hardening.sh) restreint le port au LAN 192.168.1.0/24 → pas d'exposition WAN. + - /data/docker/photoprism/originals:/photoprism/originals + - /data/docker/photoprism/storage:/photoprism/storage + - /data/docker/photoprism/import:/photoprism/import + # Bind 0.0.0.0 + ufw (01-hardening.sh) restreint le port au LAN → pas d'exposition WAN. ports: - "0.0.0.0:2342:2342" depends_on: - - photoprism_db + db: + condition: service_healthy - photoprism_db: + db: image: mariadb:10.11 container_name: photoprism_db restart: unless-stopped labels: - "com.centurylinklabs.watchtower.enable=false" + command: > + --innodb-buffer-pool-size=256M + --transaction-isolation=READ-COMMITTED + --character-set-server=utf8mb4 + --collation-server=utf8mb4_unicode_ci + --max-connections=512 + --innodb-rollback-on-timeout=OFF + --innodb-lock-wait-timeout=120 environment: - MARIADB_ROOT_PASSWORD_FILE=/run/secrets/rootpass - MARIADB_DATABASE=photoprism @@ -44,3 +63,9 @@ services: volumes: - ./db:/var/lib/mysql - ./secrets:/run/secrets:ro + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s diff --git a/g9-migration/journal.md b/g9-migration/journal.md index 7bf3a34..99e0b1b 100644 --- a/g9-migration/journal.md +++ b/g9-migration/journal.md @@ -82,3 +82,19 @@ | 2026-10-11 07:56 | 4 | migrate:vaultwarden : backup restic pré-migration | Garde-fou G2 avant import Vaultwarden | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget && restic prune | | 2026-10-11 07:57 | 4 | migrate:vaultwarden : coffre importé + service lancé | T4.1 (double-run G4) | Source stoppée → tar/ssh → **source relancée healthy** ; 621 fichiers sha256 identiques (source == stage == G9) ; G9 = 953 ciphers / 1 user (identiques à .25, revérifié après coup : .25 healthy 953) ; migrations diesel OK (v.20260902120000) ; alive 200, WebVault 200 ; DOMAIN conservé = `pass.folzocorp.fr` (clients existants intacts) ; image épinglée 1.37.4 (génération source, image .25 pullée 2026-10-05) | docker compose -f /data/docker/vaultwarden/docker-compose.yml down + rm -rf /data/docker/vaultwarden ; .25 intact | | 2026-10-11 08:01 | 4 | migrate:vaultwarden : 2FA + ADMIN_TOKEN + emergency access | T4.1 (critères 2FA + emergency access) | **2FA TOTP déjà actif côté source et importé avec le coffre** : twofactor(atype=0, enabled=1, last_used=59703916) identique sur le G9 → l'appli d'authentification de l'utilisateur continue sans re-scannage ; ADMIN_TOKEN **haché argon2id** (m=64MiB,t=3,p=4 — preset bitwarden, python3-argon2) : avis « plain text ADMIN_TOKEN » disparu, login admin 200 (dashboard Users/Invitations/Organizations/**Emergency** accessibles) ; token clair dans `/data/docker/vaultwarden/secrets/admin-plain` (600 root, à consigner dans le coffre puis supprimer) ; `EMERGENCY_ACCESS_ALLOWED=true` + `INVITATIONS_ALLOWED=true` posés | restaurer secrets/admin (token clair) + restart ; retirer les 2 env | +| 2026-10-11 08:13 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget && restic prune | +| 2026-10-11 08:16 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget && restic prune | +| 2026-10-11 08:21 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget && restic prune | +| 2026-10-11 08:28 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget && restic prune | +| 2026-10-11 08:35 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget && restic prune | +| 2026-10-11 08:40 | 4 | audit .25 : « disparition » de /volume1/docker-REC = VUE de partage DSM, pas une perte | Inventaire d'abord pris sur /volume1/docker-REC (60 entrées), puis 10 entrées une heure plus tard, propriétaire de « . » passé de Master à root, mtime /volume1 = 03:01 | `docker inspect` = vérité terrain : nextcloud_app → **/volume2/docker-REC/NextCloud/{data,config}** (+ volume nommé /volume1/@docker/volumes/f5f0…/_data), vaultwarden-server-1 → **/volume1/docker/vaultwarden**, gitea → /volume1/docker/Gitea/data. /volume1/docker-REC est une vue multi-volumes du partage « docker-REC », remontée à 03:01. **Aucune donnée perdue** : /volume2/docker-REC/NextCloud/data = 1.9 Go / 2608 fichiers ; db.sqlite3 de vaultwarden sha256 `2cad5eea42ef4b04…` **identique** entre .25 et le G9 | lecture seule, aucune action ; chemins canoniques consignés dans 07c (XP_DATA/XP_CONF = /volume2/…) | +| 2026-10-11 08:42 | 4 | kit : lancer les scripts de migration en **folzo**, jamais en `sudo` global | `sudo` remplace $HOME par /root → « Identity file /root/.ssh/id_ed25519_openfox11 not accessible » puis « Host key verification failed » (root n'a ni clé ni known_hosts) ; 3 runs de 07c échoués en préflight | 07c réécrit : appels « G9 » en **local** (`sh -c` + sudo local, la machine de pilotage EST le G9), clé en chemin absolu ; import du dump via **MARIADB_USER/MARIADB_PASSWORD** (env du conteneur MariaDB G9, pas MYSQL_*) | relancer en root avec SSH_KEY= explicite si besoin | +| 2026-10-11 09:03 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget && restic prune | +| 2026-10-11 09:10 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget && restic prune | +| 2026-10-11 09:18 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget && restic prune | +| 2026-10-11 10:10 | 4 | clarification 08:42 : le 07c **déposé** est la variante SSH, pas « local » | La ligne 08:42 décrit une réécriture « appels G9 en local » jamais commitée ; le `07c-migrate-nextcloud.sh` du dépôt passe TOUTES les ops G9 par SSH (folzo@.24, clé sur le pilot) — cohérent avec son en-tête | 07c déposé = variante SSH : préflight SSH .25 + .24, import rsync/ssh, restore via MARIADB_USER + secrets/dbpass. La formulation « local » de 08:42 ne décrit PAS le code du dépôt. + garde-fou idempotence backup (07c/07d) + nettoyage dumps (07c) + n8n.env builder (03b) + mount secrets collabora + label watchtower collabora=false | aucune — ligne documentaire | +| 2026-10-11 10:46 | 4 | récupération du thread Phase 4 supprimé (session `e67f1166…`) | suppression accidentelle de la session 07:20 UTC (cascade `events`) ; le runner a continué à travailler (journal 09:18→10:13) | thread réinséré : **130 messages / 64 657 événements / 33,85 Mo** ; caravage des pages SQLite libérées = **51 420 payloads JSON (34,68 Mo)** + 79 830 entrées d'index ; **~35 % du volume**, prose en fragments désordonnés, messages utilisateur quasi tous perdus ; intégrité `ok` avant/après ; rapport complet `g9-migration/PHASE4-RECOLLEMENT.md` | chirurgical : `DELETE FROM events WHERE session_id='e67f1166…'; DELETE FROM sessions WHERE id='e67f1166…'` — ou restauration `snap2` (voir §4 du rapport) | +| 2026-10-11 11:05 | 4 | corrections post-revue code (5 findings) | Revue de code du diff Phase 4 | 1 **sécurité** : `collabora` ne lisait PAS `password_file` (non supporté par collabora/code) → console 9980 sans mot de passe ; remplacé par `env_file: ./secrets/collabora.env` (`password=…`) généré par 03b (pattern n8n) ; 2 **import 07c** : DEFINER retirés du dump + import en **root** (rootpass) + **rc contrôlé** (l'ancien `\| tail -5` masquait ERROR 1227) ; 3 07c : `SRC_USERS` avec `\|\| true` (préflight tué par grep rc=1) ; 4 07d : progression `tar --checkpoint=50000` sur la copie 530 Go ; 5 README : arborescence + ligne phase 4 = 07a–07d (07-migrate-stateful = générique rsync, inopérant sur DSM) ; 6 `PHASE4-RECOLLEMENT.md` dégraissé (821→75 l., dump §5 hors dépôt). Vérif : `bash -n` 3/3 OK, YAML compose OK (env_file présent, password_file absent), sed DEFINER testé | `git checkout` des fichiers modifiés (compose nextcloud, 03b, 07c, 07d, README) | +| 2026-10-11 12:44 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget && restic prune | +| 2026-10-11 10:45 | 4 | migrate:nextcloud : migration complète (07c) | T4.2 (double-run G4) | export app stoppée (db vivante, mysqldump --single-transaction 31,9 Mo) + tars data (1,9 Go, 2608 fichiers sha256 identiques) + config + custom_apps → **app .25 relancée healthy** ; import G9 : MariaDB 10.11 (utf8mb4_bin, DEFINER retirés, import root, filecache=2727, users=8), occ status installed/maintenance off, version 35.0.1.1 = image épinglée, occ upgrade « No upgrade required », occ app:list **identique à la source (66 apps)** ; overrides LAN dans config/g9-double-run.config.php (réversible) ; dumps supprimés sur les 3 machines (hashs mots de passe users) | docker compose -f /data/docker/nextcloud/docker-compose.yml down + rm -rf /data/docker/nextcloud ; .25 intact | +| 2026-10-11 11:02 | 4 | migrate:nextcloud : Collabora — TLS interne désactivé | L'image collabora/code:26.04 sert **TLS par défaut** : discovery http = 000, https -k = 200 ; env `extra_features`/`EXTRA_FEATURES` et `O:ssl.enable` **non appliqués** (mesuré : entrypoint = coolwsd --use-env-vars sans script) | fix = **command: --o:ssl.enable=false** (CMD appendu à l'entrypoint) → logs « SSL is disabled », discovery http **200** ; wopi_url = http://192.168.1.24:9980 (occ, persisté) ; Caddy (T4.6) terminera le TLS | retirer la ligne command: du compose + restart | diff --git a/g9-migration/scripts/03b-secrets.sh b/g9-migration/scripts/03b-secrets.sh index 66ed42e..3bfd747 100755 --- a/g9-migration/scripts/03b-secrets.sh +++ b/g9-migration/scripts/03b-secrets.sh @@ -11,10 +11,12 @@ # # Génère (chmod 644 root:root — voir la note du code : les entrypoints conteneurs ne sont # pas root, 600 les rendrait illisibles ; le dossier reste hors git, G9 respecté) pour chaque brique : -# nextcloud : dbpass, rootpass +# nextcloud : dbpass, rootpass, collabora_admin + collabora.env (construit depuis +# collabora_admin — requis par env_file du compose : collabora/code ne lit +# PAS password_file, seul `password` est substitué dans coolwsd.xml) # photoprism : admin, dbpass, rootpass # vaultwarden : admin -# n8n : dbpass +# n8n : dbpass + n8n.env (construit depuis dbpass — requis par env_file du compose) # gitea : dbpass (backend Postgres — voir compose/gitea) # (jellyfin, watchtower : pas de secret fichier) # @@ -58,7 +60,7 @@ gen_secret() { # briques : "brique:s1 s2 s3;..." declare -A SECRET_MAP=( - [nextcloud]="dbpass rootpass" + [nextcloud]="dbpass rootpass collabora_admin" [photoprism]="admin dbpass rootpass" [vaultwarden]="admin" [n8n]="dbpass" @@ -74,6 +76,30 @@ for brique in nextcloud photoprism vaultwarden n8n gitea; do for s in ${SECRET_MAP[$brique]}; do gen_secret "$SECRETS_DIR/$s" done + if [[ "$brique" == "n8n" ]]; then + ENVF="$SECRETS_DIR/n8n.env" + if [[ -f "$ENVF" ]]; then + log " ⏭ existe déjà : $ENVF (non modifié)" + else + umask 077 + { echo "DB_POSTGRESDB_PASSWORD=$(cat "$SECRETS_DIR/dbpass")"; } > "$ENVF" + chmod 644 "$ENVF" + log " ✅ généré : $ENVF (env_file requis par compose/n8n)" + fi + fi + if [[ "$brique" == "nextcloud" ]]; then + # collabora/code ne lit PAS password_file : seul `password` est substitué dans + # coolwsd.xml → console admin 9980 protégée par env_file (pattern n8n). + ENVF="$SECRETS_DIR/collabora.env" + if [[ -f "$ENVF" ]]; then + log " ⏭ existe déjà : $ENVF (non modifié)" + else + umask 077 + { echo "password=$(cat "$SECRETS_DIR/collabora_admin")"; } > "$ENVF" + chmod 644 "$ENVF" + log " ✅ généré : $ENVF (env_file requis par compose/nextcloud → collabora)" + fi + fi done journal "Secrets bootstrap OK" "Briques stateful nécessitent des fichiers secrets" "nextcloud, photoprism, vaultwarden, n8n (chmod 644 root:root, dossier hors git)" "suppression des fichiers secrets générés" diff --git a/g9-migration/scripts/07-migrate-stateful.sh b/g9-migration/scripts/07-migrate-stateful.sh index 11a8365..35052f9 100755 --- a/g9-migration/scripts/07-migrate-stateful.sh +++ b/g9-migration/scripts/07-migrate-stateful.sh @@ -20,6 +20,15 @@ # - 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 diff --git a/g9-migration/scripts/07c-migrate-nextcloud.sh b/g9-migration/scripts/07c-migrate-nextcloud.sh new file mode 100755 index 0000000..78dd76f --- /dev/null +++ b/g9-migration/scripts/07c-migrate-nextcloud.sh @@ -0,0 +1,223 @@ +#!/usr/bin/env bash +# +# 07c-migrate-nextcloud.sh — T4.2 : migration Nextcloud (xpenology .25 → G9 .24) +# +# Garde-fous : G4 (double-run : source stoppée pendant l'export puis **relancée**, rien supprimé), +# G2 (backup restic avant import), G7 (rollback), G8 (journal), G9 (aucun secret loggué/commité). +# +# SOURCE mesurée (2026-10-11) — 3 mounts du conteneur nextcloud_app : +# /volume2/docker-REC/NextCloud/data → /var/www/html/data (datadirectory, 1.9 Go) +# /volume1/@docker/volumes//_data → /var/www/html (VOLUME NOMMÉ = code app + +# custom_apps : calendar, contacts, mail, deck, notes, +# richdocuments, collectives, forms, spreed, whiteboard…) +# /volume2/docker-REC/NextCloud/config → /var/www/html/config (config.php, redis, smtp…) +# DB : conteneur nextcloud_db (mariadb 10.6), base nextcloud / user nextcloud → mysqldump. +# +# POINTS CLÉS (C-T4.2) : +# - config.php importé impose dbhost=`db`, dbname/dbuser=nextcloud → le compose G9 nomme son +# service DB `db` et reçoit le **mot de passe d'origine** (lu depuis l'env du conteneur +# source, écrit en 644 dans secrets/dbpass, JAMAIS affiché). +# - image épinglée nextcloud:35.0.1 = version source exacte → pas de migration de schéma. +# - custom_apps copiés depuis le volume nommé : sans eux la base déclarerait des apps absents. +# +# Usage : sur la **machine de pilotage** (celle qui détient la clé .25/.24, ici /home/effi), +# ./07c-migrate-nextcloud.sh [SSH_KEY= pour forcer la clé] +# Les ops G9 passent par SSH (folzo@.24, sudo pour la partie root) : la clé vit sur le pilot, +# PAS sur le G9. Ne PAS lancer via `sudo` global : $HOME devient /root → clé et known_hosts +# introuvables → « Host key verification failed » (mesuré 2026-10-11, 3 runs échoués). +# Rollback : G9 `docker compose -f /data/docker/nextcloud/docker-compose.yml down` + +# `rm -rf /data/docker/nextcloud` ; .25 intact (services relancés). + +set -euo pipefail + +XP_USER="Master"; XP_HOST="192.168.1.25"; XP_PORT=222 +XP_DOCKER="/var/packages/ContainerManager/target/usr/bin/docker" +# On stoppe **l'app** (writes quiescents) mais PAS la DB : mysqldump a besoin du serveur +# tournant, et --single-transaction fournit l'instantané cohérent (G4 : relancée après). +XP_CONTAINERS="nextcloud_app" +XP_DATA="/volume2/docker-REC/NextCloud/data" +XP_CONF="/volume2/docker-REC/NextCloud/config" +XP_VOL_APP="/volume1/@docker/volumes/f5f099b69018be044f00d3d68f1e0da76cecb42bcb3b621b43d31091e33e789d/_data" +G9_USER="folzo"; G9_HOST="192.168.1.24" +KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519_openfox11}" +BRIQUE="nextcloud" +G9_DIR="/data/docker/$BRIQUE" +KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +KIT_ON_G9="/home/folzo/g9-migration" +JOURNAL="$KIT_DIR/journal.md" +STAGE="${STAGE:-$HOME/migration-staging/$BRIQUE}" + +SSH_XP="ssh -p $XP_PORT -i $KEY -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600 $XP_USER@$XP_HOST" +SSH_G9="ssh -i $KEY -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600 $G9_USER@$G9_HOST" +RSH_G9="ssh -i $KEY" + +log() { echo -e "\n[nextcloud] $*"; } +journal() { + printf '| %s | 4 | migrate:%s : %s | %s | %s | %s |\n' \ + "$(date '+%Y-%m-%d %H:%M')" "$BRIQUE" "$1" "$2" "$3" "$4" >> "$JOURNAL" +} +manifest_in() { ( cd "$1" && find . -type f -exec sha256sum {} + 2>/dev/null | LC_ALL=C sort ); } +verify_pair() { # $1 src .25 (sudo find), $2 dir local + local r l rc=0; r=$(mktemp); l=$(mktemp) + $SSH_XP "sudo sh -c \"cd '$1' && find . -type f -exec sha256sum {} + 2>/dev/null | LC_ALL=C sort\"" > "$r" + manifest_in "$2" > "$l" + diff -u "$r" "$l" > "$r.diff" || rc=1 + if [[ $rc -ne 0 ]]; then log " ❌ intégrité $2 :"; head -20 "$r.diff"; rm -f "$r" "$l" "$r.diff"; exit 1; fi + log " ✅ $(wc -l < "$r") fichiers identiques ($(basename "$2"))" + rm -f "$r" "$l" "$r.diff" +} + +# ---------------------------------------------------------------- 0. préflight +log "=== 0/6 PRÉFLIGHT ===" +$SSH_XP "command -v tar >/dev/null && sudo tar --version >/dev/null" || { echo "ERREUR : SSH/tar .25"; exit 1; } +$SSH_G9 "command -v docker >/dev/null && command -v rsync >/dev/null" || { echo "ERREUR : docker/rsync .24"; exit 1; } +$SSH_G9 "test -d $KIT_ON_G9/compose" || { echo "ERREUR : kit absent sur le G9"; exit 1; } +for d in "$XP_DATA" "$XP_CONF" "$XP_VOL_APP/custom_apps"; do + $SSH_XP "sudo test -d '$d'" || { echo "ERREUR : source absente sur .25 : $d"; exit 1; } +done +mkdir -p "$STAGE" + +SRC_USERS=$($SSH_XP "sudo $XP_DOCKER exec nextcloud_app php occ user:list 2>/dev/null | grep -c '^\s*-' || true") +log "source .25 : users occ = $SRC_USERS" + +# ---------------------------------------------------------------- 1. backup G2 +log "=== 1/6 BACKUP restic (G2) ===" +if $SSH_G9 "sudo restic snapshots 2>/dev/null | grep -q '$(date '+%Y-%m-%d')'"; then + log " ⏭ snapshot du jour déjà présent — backup complet sauté (idempotence)" +else + $SSH_G9 "sudo /usr/local/bin/restic-daily.sh >/dev/null 2>&1; sudo restic snapshots 2>/dev/null | tail -2" + journal "backup restic pré-migration" "Garde-fou G2 avant import Nextcloud" "snapshot restic Freebox (log /var/log/restic-daily.log)" "restic forget && restic prune" +fi + +# ---------------------------------------------------------------- 2. export (stop → export → start) +log "=== 2/6 EXPORT : stop app → mysqldump (db vivante, --single-transaction) + tars → relance app (G4) ===" +STOPPED=() +for c in $XP_CONTAINERS; do + if $SSH_XP "sudo $XP_DOCKER ps -q -f name=^${c}\$" | grep -q .; then + $SSH_XP "sudo $XP_DOCKER stop $c >/dev/null" && STOPPED+=("$c") && log " ⏸ source stoppée : $c" + fi +done +restart_source() { + for c in "${STOPPED[@]:-}"; do + [[ -n "$c" ]] || continue + $SSH_XP "sudo $XP_DOCKER start $c >/dev/null" && log " ▶ source redémarrée : $c" \ + || log " ⚠️ ÉCHEC redémarrage $c — INTERVENIR SUR .25" + done +} +trap restart_source EXIT + +DUMP=/tmp/nc_dump.sql +$SSH_XP "sudo $XP_DOCKER exec nextcloud_db sh -c 'mysqldump -u\"\$MYSQL_USER\" -p\"\$MYSQL_PASSWORD\" --single-transaction --routines --triggers --events --default-character-set=utf8mb4 \"\$MYSQL_DATABASE\"' > $DUMP && sudo chmod 644 $DUMP" +log " dump : $($SSH_XP "stat -c%s $DUMP") o, sha256 $($SSH_XP "sha256sum $DUMP | cut -c1-16")…" +# mot de passe DB d'origine : lu depuis l'env du conteneur, écrit en 600 local, jamais affiché +$SSH_XP "sudo $XP_DOCKER inspect nextcloud_db --format '{{range .Config.Env}}{{println .}}{{end}}'" \ + | sed -n 's/^MYSQL_PASSWORD=//p' > "$STAGE/dbpass"; chmod 600 "$STAGE/dbpass" +[[ -s "$STAGE/dbpass" ]] || { echo "ERREUR : mot de passe DB source non récupéré"; exit 1; } + +$SSH_XP "sudo tar cz -C '$XP_DATA' ." > "$STAGE/data.tgz" +$SSH_XP "sudo tar cz -C '$XP_CONF' ." > "$STAGE/config.tgz" +$SSH_XP "sudo tar cz -C '$XP_VOL_APP' custom_apps" > "$STAGE/custom_apps.tgz" +log " archives : data=$(du -h "$STAGE/data.tgz"|cut -f1) config=$(du -h "$STAGE/config.tgz"|cut -f1) custom_apps=$(du -h "$STAGE/custom_apps.tgz"|cut -f1)" +mkdir -p "$STAGE/x_data" "$STAGE/x_config" "$STAGE/x_custom_apps" +tar xzf "$STAGE/data.tgz" -C "$STAGE/x_data" +tar xzf "$STAGE/config.tgz" -C "$STAGE/x_config" +tar xzf "$STAGE/custom_apps.tgz" -C "$STAGE/x_custom_apps" +verify_pair "$XP_DATA" "$STAGE/x_data" +verify_pair "$XP_CONF" "$STAGE/x_config" +verify_pair "$XP_VOL_APP/custom_apps" "$STAGE/x_custom_apps/custom_apps" +rm -f "$STAGE"/*.tgz + +restart_source; STOPPED=() + +# ---------------------------------------------------------------- 3. import G9 +log "=== 3/6 IMPORT sur le G9 ===" +$SSH_G9 "sudo mkdir -p $G9_DIR/data/data && sudo chown -R $G9_USER:$G9_USER $G9_DIR" +# MariaDB tourne en uid 999 : le datadir doit lui appartenir (sinon « Permission denied » sur +# db.opt, mesuré 2026-10-11). Le chown -R folzo ci-dessus est nécessaire au rsync, pas à la DB. +$SSH_G9 "sudo mkdir -p $G9_DIR/db && sudo chown -R 999:999 $G9_DIR/db && sudo chmod 750 $G9_DIR/db" +rsync -a --info=stats1 -e "$RSH_G9" "$STAGE/x_data/" "$G9_USER@$G9_HOST:$G9_DIR/data/data/" +rsync -a --info=stats1 -e "$RSH_G9" "$STAGE/x_config/" "$G9_USER@$G9_HOST:$G9_DIR/config/" +rsync -a --info=stats1 -e "$RSH_G9" "$STAGE/x_custom_apps/" "$G9_USER@$G9_HOST:$G9_DIR/data/" +g=$(mktemp); l2=$(mktemp); manifest_in "$STAGE/x_data" > "$l2" +$SSH_G9 "cd '$G9_DIR/data/data' && find . -type f -exec sha256sum {} + 2>/dev/null | LC_ALL=C sort" > "$g" +diff -u "$l2" "$g" > "$g.diff" || { log "❌ intégrité data sur le G9 :"; head -20 "$g.diff"; exit 1; } +log " ✅ data G9 : $(wc -l < "$g") fichiers identiques" +rm -f "$l2" "$g" "$g.diff" + +# ---------------------------------------------------------------- 4. compose + secrets + restore DB +log "=== 4/6 COMPOSE + SECRETS + RESTORE DB ===" +rsync -a --info=stats1 -e "$RSH_G9" --exclude "secrets" "$KIT_DIR/compose/$BRIQUE/" "$G9_USER@$G9_HOST:$KIT_ON_G9/compose/$BRIQUE/" +rsync -a -e "$RSH_G9" "$KIT_DIR/scripts/03b-secrets.sh" "$G9_USER@$G9_HOST:$KIT_ON_G9/scripts/" +$SSH_G9 "sudo bash $KIT_ON_G9/scripts/03b-secrets.sh >/dev/null" +$SSH_G9 "sudo rsync -a $KIT_ON_G9/compose/$BRIQUE/ $G9_DIR/ && sudo chmod 755 $G9_DIR/secrets" +# dbpass = mot de passe D'ORIGINE (config.php importé), rootpass = généré par 03b-secrets. +# secrets/ est root:root sur le G9 → passage par /tmp puis install en sudo (jamais affiché). +rsync -a -e "$RSH_G9" "$STAGE/dbpass" "$G9_USER@$G9_HOST:/tmp/.nc-dbpass" +$SSH_G9 "sudo install -m 644 -o root -g root /tmp/.nc-dbpass $G9_DIR/secrets/dbpass && rm -f /tmp/.nc-dbpass && sudo chmod 644 $G9_DIR/secrets/rootpass $G9_DIR/secrets/collabora_admin 2>/dev/null || true" +rm -f "$STAGE/dbpass"; rm -rf "$STAGE" + +$SSH_G9 "cd $G9_DIR && docker compose up -d db redis && sleep 25 && docker compose ps --format '{{.Name}} {{.Status}}'" +log " restore du dump (base vierge, schéma oc_ + utf8mb4)" +$SSH_G9 "cd $G9_DIR && docker compose exec -T db sh -c 'mysql -u\"\$MARIADB_USER\" -p\"\$(cat /run/secrets/dbpass)\" -e \"ALTER DATABASE nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;\"' < /dev/null" +$SSH_XP "sudo cat $DUMP" > /tmp/nc_dump_local.sql +# DEFINER=`nextcloud`@`…` (routines/triggers/events, issus de --routines --triggers --events) +# importés en utilisateur non-root → « ERROR 1227 … you need SUPER » sous MariaDB 10.11. +# → DEFINER retirés localement, import en **root** (rootpass, seedé par 03b), rc contrôlé +# (l'ancien `2>&1 | tail -5` masquait l'échec et le script poursuivait). +DEF_COUNT=$(grep -c 'DEFINER=' /tmp/nc_dump_local.sql || true) +sed -E 's/DEFINER=`[^`]+`@`[^`]+`//g' /tmp/nc_dump_local.sql > /tmp/nc_dump_stripped.sql +log " DEFINER retirés du dump : $DEF_COUNT" +rsync -a --info=stats1 -e "$RSH_G9" /tmp/nc_dump_stripped.sql "$G9_USER@$G9_HOST:$G9_DIR/nc_dump.sql" +IMPORT_LOG=/tmp/nc_import.log +if ! $SSH_G9 "cd $G9_DIR && docker compose exec -T db sh -c 'mysql -u root -p\"\$(cat /run/secrets/rootpass)\" nextcloud' < $G9_DIR/nc_dump.sql" > "$IMPORT_LOG" 2>&1; then + log "❌ import du dump en échec (rc≠0) — 20 dernières lignes de la sortie mysql :" + tail -20 "$IMPORT_LOG" + exit 1 +fi +if grep -qiE '^ERROR [0-9]+' "$IMPORT_LOG"; then + log "❌ erreurs MySQL détectées pendant l'import :" + grep -iE '^ERROR [0-9]+' "$IMPORT_LOG" | head -10 + exit 1 +fi +log " import OK en root (base vierge, DEFINER retirés) — $(tail -1 "$IMPORT_LOG")" +rm -f /tmp/nc_dump_local.sql /tmp/nc_dump_stripped.sql "$IMPORT_LOG" +$SSH_G9 "cd $G9_DIR && docker compose exec -T db sh -c 'mysql -u\"\$MARIADB_USER\" -p\"\$(cat /run/secrets/dbpass)\" nextcloud -e \"select count(*) as filecache from oc_filecache; select count(*) as users from oc_users;\"' < /dev/null" +# nettoyage des dumps (hashs des mots de passe users) — G9 : rien ne traîne (les 3 copies) +$SSH_XP "sudo rm -f $DUMP" +rm -f /tmp/nc_dump_local.sql +$SSH_G9 "rm -f $G9_DIR/nc_dump.sql" +log " dumps nettoyés (.25, pilot, G9)" + +# override LAN du double-run (documenté, réversible) : pas de réécriture https, LAN en confiance +$SSH_G9 "cat > $G9_DIR/config/g9-double-run.config.php" <<'PHP' + array (0 => 'nextcloud.folzocorp.fr', 1 => '192.168.1.24', 2 => '192.168.1.24:8080'), + 'overwritehost' => '', + 'overwriteprotocol' => '', + 'overwrite.cli.url' => 'http://192.168.1.24:8080', + 'trusted_proxies' => array (0 => '192.168.1.0/24'), +); +PHP + +$SSH_G9 "sudo chown -R 33:33 $G9_DIR/data $G9_DIR/config && sudo chmod -R u+rwX $G9_DIR/data $G9_DIR/config" +$SSH_G9 "cd $G9_DIR && docker compose up -d nextcloud && sleep 30 && docker compose ps --format '{{.Name}} {{.Status}}'" +log " occ status / upgrade" +$SSH_G9 "cd $G9_DIR && docker compose exec -T nextcloud su -s /bin/sh -c 'php occ status' www-data < /dev/null 2>&1 | head -8" +$SSH_G9 "cd $G9_DIR && docker compose exec -T nextcloud su -s /bin/sh -c 'php occ upgrade' www-data < /dev/null 2>&1 | tail -6" +$SSH_G9 "cd $G9_DIR && docker compose exec -T nextcloud su -s /bin/sh -c 'php occ maintenance:repair --include-expensive' www-data < /dev/null 2>&1 | tail -4" + +# ---------------------------------------------------------------- 5. Collabora +log "=== 5/6 COLLABORA ===" +$SSH_G9 "cd $G9_DIR && docker compose up -d collabora && sleep 25 && curl -sf -o /dev/null -w 'collabora discovery HTTP %{http_code}\n' http://127.0.0.1:9980/hosting/discovery" +$SSH_G9 "cd $G9_DIR && docker compose exec -T nextcloud su -s /bin/sh -c 'php occ config:app:set richdocuments wopi_url --value=http://192.168.1.24:9980 && php occ config:app:get richdocuments wopi_url' www-data < /dev/null 2>&1 | tail -2" + +# ---------------------------------------------------------------- 6. vérification +log "=== 6/6 VÉRIFICATION ===" +$SSH_G9 "curl -sf -o /dev/null -w 'nextcloud status.php HTTP %{http_code}\n' http://127.0.0.1:8080/status.php" +$SSH_G9 "cd $G9_DIR && docker compose exec -T nextcloud su -s /bin/sh -c 'php occ user:list' www-data < /dev/null 2>&1 | grep -c '^\s*-'" +$SSH_XP "sudo $XP_DOCKER ps --format '{{.Names}} {{.Status}}' | grep -iE 'nextcloud'" +journal "Nextcloud importé + lancé sur le G9 (+ Collabora)" "T4.2 (double-run G4)" "data 1.9 Go + config + custom_apps + mysqldump sha256 identiques ; occ status/upgrade OK ; Collabora discovery OK ; wopi_url → G9 ; source relancée" "docker compose -f $G9_DIR/docker-compose.yml down + rm -rf $G9_DIR ; .25 intact" + +log "Terminé." diff --git a/g9-migration/scripts/07d-migrate-photoprism.sh b/g9-migration/scripts/07d-migrate-photoprism.sh new file mode 100755 index 0000000..9fe743d --- /dev/null +++ b/g9-migration/scripts/07d-migrate-photoprism.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# 07d-migrate-photoprism.sh — T4.3 : PhotoPrism sur le G9, photos COPIÉES (décision #3). +# +# Vérité terrain mesurée 2026-10-11 : il n'y a **aucun conteneur ni image PhotoPrism sur .25** +# (`docker ps -a` : rien, `docker images` : rien). PhotoPrism n'est donc pas « migré » : +# il est **installé** sur le G9 à partir des photos du NAS (/volume1/photo = 530 Go, +# 305 059 fichiers media, 2002→2026), avec un index neuf (décision #4 : indexation progressive). +# +# Transport : le sshd de DSM refuse rsync --server et scp (voir journal 2026-10-11) → +# flux tar sur SSH, relayé par la machine de pilotage : .25 → pilot → .24. +# +# Intégrité : manifeste **métadonnées** (chemin + taille, LC_ALL=C) source vs G9, totaux +# (nb fichiers, octets), plus échantillon sha256 de 200 fichiers. +# +# Usage : sur la machine de pilotage (détenant la clé), ./07d-migrate-photoprism.sh +# Étape longue (530 Go). `SKIP_COPY=1` pour ne redéployer que la stack. +# Rollback : `docker compose -f /data/docker/photoprism/docker-compose.yml down` + +# `rm -rf /data/docker/photoprism` ; .25 intact (lecture seule sur /volume1/photo). + +set -euo pipefail + +XP_USER="Master"; XP_HOST="192.168.1.25"; XP_PORT=222 +XP_PHOTO="/volume1/photo" +G9_USER="folzo"; G9_HOST="192.168.1.24" +KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519_openfox11}" +BRIQUE="photoprism" +G9_DIR="/data/docker/$BRIQUE" +KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +KIT_ON_G9="/home/folzo/g9-migration" +JOURNAL="$KIT_DIR/journal.md" +STAGE="${STAGE:-$HOME/migration-staging/$BRIQUE}" +MIN_FREE_GB=600 +INDEX_TIMEOUT="${INDEX_TIMEOUT:-3600}" + +SSH_XP="ssh -p $XP_PORT -i $KEY -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600 $XP_USER@$XP_HOST" +SSH_G9="ssh -i $KEY -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600 $G9_USER@$G9_HOST" +RSH_G9="ssh -i $KEY" + +log() { echo -e "\n[photoprism] $*"; } +journal() { + printf '| %s | 4 | migrate:%s : %s | %s | %s | %s |\n' \ + "$(date '+%Y-%m-%d %H:%M')" "$BRIQUE" "$1" "$2" "$3" "$4" >> "$JOURNAL" +} +meta_manifest() { ( cd "$1" && find . -type f -printf '%p\t%s\n' 2>/dev/null | LC_ALL=C sort ); } + +# ---------------------------------------------------------------- 0. préflight +log "=== 0/5 PRÉFLIGHT ===" +$SSH_XP "command -v tar >/dev/null && sudo tar --version >/dev/null" || { echo "ERREUR : SSH/tar .25"; exit 1; } +$SSH_G9 "command -v docker >/dev/null && command -v rsync >/dev/null" || { echo "ERREUR : docker/rsync .24"; exit 1; } +$SSH_XP "sudo test -d $XP_PHOTO" || { echo "ERREUR : $XP_PHOTO absent sur .25"; exit 1; } +FREE_GB=$($SSH_G9 "df -BG --output=avail /data | tail -1 | tr -dc '0-9'") +[[ "$FREE_GB" -ge "$MIN_FREE_GB" ]] || { echo "ERREUR : espace /data insuffisant (${FREE_GB} Go < ${MIN_FREE_GB} Go)"; exit 1; } +log "espace /data = ${FREE_GB} Go" +mkdir -p "$STAGE" + +SRC_COUNT=$($SSH_XP "sudo find $XP_PHOTO -type f | wc -l") +SRC_BYTES=$($SSH_XP "sudo find $XP_PHOTO -type f -printf '%s\n' | awk '{s+=\$1} END {print s}'") +log "source .25 : $SRC_COUNT fichiers, $SRC_BYTES octets" + +# ---------------------------------------------------------------- 1. backup G2 +log "=== 1/5 BACKUP restic (G2) ===" +if $SSH_G9 "sudo restic snapshots 2>/dev/null | grep -q '$(date '+%Y-%m-%d')'"; then + log " ⏭ snapshot du jour déjà présent — backup complet sauté (idempotence)" +else + $SSH_G9 "sudo /usr/local/bin/restic-daily.sh >/dev/null 2>&1; sudo restic snapshots 2>/dev/null | tail -2" + journal "backup restic pré-migration" "Garde-fou G2 avant import PhotoPrism" "snapshot restic Freebox (log /var/log/restic-daily.log)" "restic forget && restic prune" +fi + +# ---------------------------------------------------------------- 2. copie des photos +if [[ "${SKIP_COPY:-0}" != "1" ]]; then + log "=== 2/5 COPIE $XP_PHOTO → $G9_DIR/originals (tar sur SSH, relais pilot) ===" + $SSH_G9 "sudo mkdir -p $G9_DIR/originals $G9_DIR/storage $G9_DIR/import && sudo chown -R $G9_USER:$G9_USER $G9_DIR" + # MariaDB = uid 999 : datadir à lui (sinon « Permission denied » sur db.opt). + $SSH_G9 "sudo mkdir -p $G9_DIR/db && sudo chown -R 999:999 $G9_DIR/db && sudo chmod 750 $G9_DIR/db" + t0=$(date +%s) + # Progression : tar GNU du G9 émet un pointage sur stderr tous les 50 000 enregistrements + # (stderr relayé par ssh) → la copie de 530 Go n'est pas silencieuse pendant des heures. + if ! $SSH_XP "sudo tar cz -C '$XP_PHOTO' ." | $SSH_G9 "tar xz --checkpoint=50000 --checkpoint-action=echo='[copie] %n enregistrements' -C $G9_DIR/originals"; then + echo "ERREUR : flux tar interrompu"; exit 1 + fi + t=$(( $(date +%s) - t0 )) + log " copie terminée en ${t} s" + journal "copie photos .25 → G9" "530 Go, source en lecture seule (aucune écriture sur .25)" "tar/ssh relayé par le pilot, ${t} s ; $SRC_COUNT fichiers source" "rm -rf $G9_DIR/originals (source .25 intacte)" +fi + +# ---------------------------------------------------------------- 3. intégrité +log "=== 3/5 INTÉGRITÉ (manifeste métadonnées + échantillon sha256) ===" +$SSH_XP "sudo sh -c \"cd '$XP_PHOTO' && find . -type f -printf '%p\t%s\n' | LC_ALL=C sort\"" > "$STAGE/src.meta" +$SSH_G9 "cd '$G9_DIR/originals' && find . -type f -printf '%p\t%s\n' | LC_ALL=C sort" > "$STAGE/g9.meta" +DST_COUNT=$(wc -l < "$STAGE/g9.meta") +DST_BYTES=$(awk -F'\t' '{s+=$2} END {print s}' "$STAGE/g9.meta") +log " source=$SRC_COUNT fichiers / $SRC_BYTES o — G9=$DST_COUNT fichiers / $DST_BYTES o" +[[ "$SRC_COUNT" == "$DST_COUNT" && "$SRC_BYTES" == "$DST_BYTES" ]] || { echo "ERREUR : compte/taille différents"; diff "$STAGE/src.meta" "$STAGE/g9.meta" | head -20; exit 1; } +diff "$STAGE/src.meta" "$STAGE/g9.meta" > "$STAGE/meta.diff" || { log "❌ chemins/tailles différents :"; head -20 "$STAGE/meta.diff"; exit 1; } +log " ✅ manifeste métadonnées identique ($DST_COUNT fichiers)" + +SAMPLE=$(shuf -n 200 "$STAGE/g9.meta" | cut -f1) +printf '%s\n' "$SAMPLE" > "$STAGE/sample.txt" +n=$(wc -l < "$STAGE/sample.txt") +SRC_SHA=$(printf '%s\n' "$SAMPLE" | $SSH_XP "sudo sh -c \"cd '$XP_PHOTO' && xargs -d '\n' sha256sum\"" | awk '{print $1}' | LC_ALL=C sort) +DST_SHA=$(printf '%s\n' "$SAMPLE" | $SSH_G9 "cd '$G9_DIR/originals' && xargs -d '\n' sha256sum" | awk '{print $1}' | LC_ALL=C sort) +if [[ "$SRC_SHA" == "$DST_SHA" ]]; then + log " ✅ échantillon sha256 : $n/$n fichiers identiques" +else + log "❌ échantillon sha256 divergent"; diff <(echo "$SRC_SHA") <(echo "$DST_SHA") | head -10; exit 1 +fi +journal "intégration PhotoPrism" "copie de 530 Go à vérifier avant indexation" "manifeste chemins+tailles identique ($DST_COUNT fichiers, $DST_BYTES o) + échantillon sha256 $n/$n" "re-copie ciblée via tar/ssh" + +# ---------------------------------------------------------------- 4. stack + secrets +log "=== 4/5 STACK + SECRETS ===" +rsync -a --info=stats1 -e "$RSH_G9" --exclude "secrets" "$KIT_DIR/compose/$BRIQUE/" "$G9_USER@$G9_HOST:$KIT_ON_G9/compose/$BRIQUE/" +rsync -a -e "$RSH_G9" "$KIT_DIR/scripts/03b-secrets.sh" "$G9_USER@$G9_HOST:$KIT_ON_G9/scripts/" +$SSH_G9 "sudo bash $KIT_ON_G9/scripts/03b-secrets.sh >/dev/null" +$SSH_G9 "sudo rsync -a $KIT_ON_G9/compose/$BRIQUE/ $G9_DIR/ && sudo chmod 755 $G9_DIR/secrets" +$SSH_G9 "sudo chown -R 999:999 $G9_DIR/db && sudo chmod 750 $G9_DIR/db" +# PhotoPrism n'accepte pas les *_FILE : env_file construit à partir des secrets (600, root, hors git). +$SSH_G9 "sudo sh -c 'umask 077; { echo PHOTOPRISM_ADMIN_PASSWORD=\$(cat $G9_DIR/secrets/admin); echo PHOTOPRISM_DATABASE_PASSWORD=\$(cat $G9_DIR/secrets/dbpass); } > $G9_DIR/secrets/photoprism.env' && sudo chown root:root $G9_DIR/secrets/photoprism.env" +$SSH_G9 "cd $G9_DIR && docker compose up -d && sleep 30 && docker compose ps --format '{{.Name}} {{.Status}}'" + +# ---------------------------------------------------------------- 5. indexation + vérifs +log "=== 5/5 INDEXATION (progressive, décision #4) + VÉRIFS ===" +$SSH_G9 "curl -sf -o /dev/null -w 'photoprism HTTP %{http_code}\n' http://127.0.0.1:2342/api/v1/session/health" +$SSH_G9 "cd $G9_DIR && timeout $INDEX_TIMEOUT docker compose exec -T photoprism photoprism index 2>&1 | tail -6 < /dev/null || true" +$SSH_G9 "cd $G9_DIR && docker compose exec -T photoprism sh -c 'photoprism count 2>/dev/null | head -12' < /dev/null || true" +journal "stack PhotoPrism déployée + indexation lancée" "T4.3 : stack saine, index progressif (photothèque 530 Go sur le G9)" "image épinglée 260919, MariaDB 10.11 (watchtower OFF), secrets via env_file 600, port 2342 LAN ; originals = copie locale" "docker compose down + rm -rf /data/docker/photoprism" + +echo +log "TERMINÉ. Prochaine étape : laisser monter l'index (cron/kommand), puis T4.5 (n8n)."