# Prompt Claude Code : déploiement complet de Syllabus > À coller dans Claude Code, lancé depuis la racine du dépôt `Paillasse` sur le Mac d'Adam. --- ## 0. Contexte **Nom du produit : Syllabus** (ancien nom de travail : Paillasse). - Tout ce que voient les utilisateurs (pages, titres, e-mails, mentions légales) dit **Syllabus**, avec les logos `design/images/logo-syllabus.png` et `design/images/logo-syllabus-icone.png`. - Les noms techniques internes restent tels quels : le dépôt `Paillasse`, le dossier `paillasse-contenus/`, l'image `paillasse-debian`, le projet OpenStack `paillasse`. Ne les renomme pas. - Si tu trouves « Paillasse » dans un texte visible, remplace-le par « Syllabus ». Tu vas déployer **Syllabus** de bout en bout sur un serveur bare metal, jusqu'à une version utilisable par une vraie classe. Lis d'abord `CLAUDE.md` et `paillasse-contenus/README.md`. Ils contiennent le contexte complet du projet, la stack et les règles. **Syllabus** : une plateforme de labs Linux et réseau pour les écoles. Chaque élève a une vraie VM Debian, avec une console dans le navigateur et une correction automatique par scripts. Pas d'IA dans le produit. **Le serveur** - Scaleway Dedibox **Start-2-L**, datacenter DC2 (France). - Xeon D-1531 (6 cœurs / 12 threads), **32 Go de RAM**, 2 × 250 Go SSD en RAID 1 (`/` fait 226 Go). - **Une seule IP publique** : `163.172.4.195`. - Debian `12`, installé par Adam depuis la console Dedibox. - Accès SSH : `ssh adam@163.172.4.195`, avec la clé dédiée `~/.ssh/id_ed25519_claude`. Crée l'alias `paillasse` dans `~/.ssh/config`, avec `IdentitiesOnly yes` pour n'utiliser que cette clé. - `sudo` fonctionne sans mot de passe pour `adam` (fichier `/etc/sudoers.d/90-adam`). - Sauvegardes : **Dedibackup 100 Go** (FTP, joignable uniquement depuis le serveur). Adam a déposé les identifiants dans `/root/.dedibackup` (`chmod 600`). Ne les affiche jamais. **Image et labs** : suis la section « Image commune » de `paillasse-contenus/README.md`, et crée ce que demandent les labs 04 et 07 (étape `preparation`, `/opt/labs/07/lycee.sql`, `/opt/paquets/`). **La forge** : `ssh://git@forge.adam.belkacemi.me:2222/adam/Paillasse.git`. Tu n'y fais que `git push` : aucune connexion SSH à la machine qui l'héberge. **Le domaine** : pas encore acheté. Fais les étapes 1 à 11 sans domaine (accès par IP ou tunnel SSH). Avant l'étape 12, arrête-toi et demande-moi le domaine. --- ## 1. Règles non négociables 1. **Ne jamais toucher à `tripoli-cloud`** ni à aucune autre machine que le serveur Syllabus et le Mac d'Adam. 2. **Aucun secret** dans le code, dans Git, dans le journal ou dans la conversation. - Les secrets vivent dans des fichiers `chmod 600` sur le serveur, ou dans un `.env` ignoré par Git. - Les secrets Ansible passent par **Ansible Vault**. Le mot de passe du vault reste hors du dépôt. - Kolla : `passwords.yml` n'est jamais commité en clair. 3. **Commandes à risque : explique, montre la commande, vérifie la sauvegarde, puis attends le « OK » d'Adam.** Sont concernés : - suppressions (`rm -rf`, `DROP`, `kolla-ansible destroy`…) ; - disques, partitions, RAID ; - toute modification du **réseau**, du **pare-feu** ou de **SSH** ; - redémarrage du serveur. 4. **Anti-coupure SSH** : avant toute modification du réseau, du pare-feu ou de SSH, programme un retour automatique à l'ancienne configuration dans 5 minutes (par exemple avec `systemd-run --on-active=5min`), puis annule-le une fois l'accès vérifié depuis une **nouvelle** session SSH. 5. **Tu ne commandes et ne paies jamais rien.** 6. **Élèves mineurs** : minimisation des données. - Un élève = un identifiant `prénom.initiale` et un code secret. Pas d'e-mail. Le nom complet est facultatif et visible uniquement du prof. - Suppression automatique 90 jours après l'archivage d'une classe. 7. **Tout est du code** : - chaque configuration passe par un rôle Ansible idempotent dans `ansible/` ; - une commande tapée à la main pour explorer doit ensuite être codifiée ; - relancer un playbook ne doit rien casser. 8. **Vérifie les versions dans la documentation officielle**, jamais de mémoire : Kolla-Ansible, OS supportés, Laravel, PHP, Go. Utilise Context7 ou le web. 9. **Documentation en français** (docs, commentaires, messages de commit). --- ## 2. Méthode de travail ### Pour chaque étape 1. Annonce l'étape et son critère de réussite. 2. Fais le travail. 3. **Vérifie le critère par un test réel** et montre la sortie. 4. Écris dans `docs/journal.md` : - ce qui a été fait ; - les commandes clés ; - les problèmes rencontrés et leur solution ; - un paragraphe **« Comprendre »** de 3 à 6 phrases, qui explique simplement le pourquoi à Adam. 5. `git commit`, puis `git push`. 6. Passe à l'étape suivante sans attendre, sauf si une règle de la section 1 impose l'accord d'Adam. ### Limite de temps **1 h 30 maximum par étape.** Si elle est dépassée : - arrête-toi ; - écris le blocage dans `docs/journal.md` (symptôme, pistes essayées, hypothèses) ; - propose 2 options à Adam et attends sa décision. ### Arborescence du dépôt ``` ansible/ inventaire, playbooks, rôles kolla/ globals.yml, inventaire all-in-one (passwords.yml : chiffré ou hors Git) openstack/ modèles Heat, flavors, scripts de construction d'images portail/ application Laravel proxy-console/ service Go labs/ copie de travail des labs YAML design/ maquettes Claude Design (référence exacte, ne pas modifier) docs/ journal.md, runbooks/, inventaire-serveur.md, post-mortems/ ``` --- ## 3. Les étapes ### Étape 1 : Reconnaissance (lecture seule) - Relève sur le serveur : - OS, noyau, CPU ; - virtualisation matérielle (`vmx`) et `/dev/kvm` ; - RAM, disques, RAID ; - interfaces réseau, IP, passerelle, DNS. - Vérifie quelle version stable de Kolla-Ansible supporte Debian 12, et choisis-la. - Écris `docs/inventaire-serveur.md`. **Critère :** inventaire écrit, KVM disponible, version de Kolla-Ansible choisie et justifiée. ### Étape 2 : Durcissement (Ansible depuis le Mac) - Installe Ansible sur le Mac s'il manque. Crée `ansible/inventory.ini` et un rôle `durcissement` : - utilisateur d'administration, `sudo` ; - SSH par clé uniquement, `PermitRootLogin no` ; - fail2ban, `unattended-upgrades`, chrony, fuseau `Europe/Paris` ; - pare-feu nftables : seuls **22, 80 et 443** ouverts sur l'IP publique. - Attention : `kolla-ansible bootstrap-servers` peut désactiver le pare-feu. Prévois-le dès maintenant (réglage Kolla adapté, ou rôle nftables rejoué après Kolla). **Critère :** - connexion par clé OK ; - connexion par mot de passe refusée, root refusé ; - depuis le Mac, `nmap 163.172.4.195` ne montre que 22, 80 et 443. ### Étape 3 : Sauvegardes vers Dedibackup - Dedibackup (`dedibackup-dc3.online.net`) est limité à **1000 fichiers** et passe par du FTP **non chiffré**. - Fais des **archives quotidiennes** `tar.zst`, **chiffrées avec age** avant l'envoi. - Rotation : 7 quotidiennes et 4 hebdomadaires. - À sauvegarder : `/etc`, `/home`, `/root`, `/etc/kolla` (plus tard), les dumps MariaDB de l'app (plus tard), le dossier du portail. - Programmation : timer systemd quotidien. - Identifiants dans `/root/.dedibackup` (variables `DEDIBACKUP_HOST`, `DEDIBACKUP_USER`, `DEDIBACKUP_PASS`). La clé privée age ne quitte jamais le serveur : rappelle-moi d'en garder une copie hors du serveur. - Écris `docs/runbooks/sauvegardes.md`. **Critère :** une sauvegarde est envoyée et **un fichier est restauré avec succès** dans `/tmp`. ### Étape 4 : Réseau pour OpenStack (accord d'Adam obligatoire) L'objectif est de ne jamais mettre Neutron sur l'interface publique. - Crée une interface `dummy0` avec une IP privée (par exemple `10.10.0.1/24`). Elle servira à `network_interface` et `kolla_internal_vip_address` (Kolla sans HAProxy). - Crée une paire veth. `neutron_external_interface` utilise un bout de la paire. L'autre bout porte la passerelle du réseau externe OpenStack, avec un NAT masquerade nftables. - Rends le tout persistant avec systemd-networkd et le rôle Ansible `reseau-openstack`. - Applique la règle anti-coupure SSH, puis **redémarre, avec l'accord d'Adam**. **Critère :** après redémarrage, le SSH répond, `dummy0` et la paire veth sont présents, et Internet reste joignable depuis le serveur. ### Étape 5 : Kolla-Ansible tout-en-un - Dans un venv Python : la version de Kolla-Ansible choisie à l'étape 1, puis `kolla-ansible install-deps`. - Dans `kolla/globals.yml` (**vérifie les noms exacts dans la doc de la version**), les idées sont : - `enable_haproxy: "no"`, `enable_horizon: "no"` ; - `enable_heat: "yes"`, `enable_nova_serialconsole_proxy: "yes"` ; - `nova_compute_virt_type: "kvm"` ; - `enable_prometheus` et `enable_grafana` si c'est le plus simple pour l'étape 7. - Mots de passe : `kolla-genpwd`. Jamais de `passwords.yml` en clair dans Git. - Lance `bootstrap-servers`, `prechecks`, `deploy`, puis `post-deploy`. Installe le client `openstack`. - Rejoue le rôle pare-feu si Kolla l'a modifié. **Critère :** - `openstack service list` et `openstack compute service list` sont OK ; - aucune API OpenStack n'est joignable depuis l'IP publique (vérifie avec `nmap`). ### Étape 6 : Première VM en console série 1. Crée le projet `paillasse` et un utilisateur de service avec des **application credentials**, stockés dans un fichier `chmod 600`. 2. Construis l'image `paillasse-debian` avec `virt-customize`, à partir de l'image cloud Debian 12 officielle, en suivant la section « Image commune » de `paillasse-contenus/README.md`. 3. Propriété de l'image : `hw_qemu_guest_agent=yes`. 4. Crée la flavor `eleve.petit` : 1 vCPU, 768 Mo, 5 Go. 5. Démarre une VM et récupère l'adresse de sa console série avec l'API Nova `remote-consoles` (protocole `serial`). **Critère :** un petit script (websocat ou Python) connecté au WebSocket de la console série affiche l'invite de connexion, et une commande tapée s'exécute. ### Étape 7 : Supervision et alertes - Prometheus, node_exporter et Grafana : ceux de Kolla si possible. - Uptime Kuma en conteneur Docker, écoutant sur `127.0.0.1`. - Alertes : disque > 80 %, RAM > 90 %, service OpenStack arrêté, dernière sauvegarde de plus de 26 h, serveur injoignable. - Notifications par webhook : l'URL est un secret, demande-la-moi. - Tant qu'il n'y a pas de domaine, accès par tunnel SSH uniquement. **Critère :** une alerte de test est **reçue** par Adam. ### Étape 8 : Portail Laravel - Nginx et PHP-FPM, avec une version de PHP compatible avec la dernière version de Laravel. - MariaDB et Redis **pour l'app** en Docker Compose, liés à `127.0.0.1`, sur des ports différents de ceux de Kolla (par exemple 3307 et 6380). - **Laravel avec Blade et Vite**, dans `portail/`. - **Design** : - le dossier `design/` est la référence exacte. Il contient déjà des vues Blade (`design/resources/views/`) et des routes (`design/routes/web.php`) : reprends-les **telles quelles** ; - le JS existant (`design/js/`, dont `paillasse-terminal.js`) et le CSS (`design/css/`) sont conservés ; - compare chaque page avec une capture Playwright de `design/`, et corrige jusqu'à ce qu'elles soient identiques ; - si `design/` est absent, arrête-toi et demande-le-moi. - Modèles : - `Etablissement`, `User` (prof), `Classe`, `Eleve` (identifiant, code secret haché, nom complet facultatif) ; - `Lab`, `Seance`, `LabSession` (élève, lab, id de la stack ou des VM, état, expiration). - Connexion, sur un seul écran `/connexion` : - le prof avec un e-mail et un mot de passe ; - l'élève avec son identifiant `prénom.initiale` (avec un numéro pour les homonymes) et un code secret de 6 caractères. - Le prof crée une classe, saisit ou importe en CSV la liste des élèves, et génère leurs identifiants et codes (fiches imprimables avec QR code). - **Une seule session active par compte** : la précédente est coupée et le prof est alerté. - **Salle d'attente de séance** : le prof admet les élèves. - Tests **Pest** pour la connexion, la session unique et la création de classe. **Critère :** les tests Pest passent, et un élève fictif se connecte et voit son tableau de bord, identique à la maquette. ### Étape 9 : Proxy console Go et xterm.js - Service Go dans `proxy-console/`, écoutant sur `127.0.0.1:8081`, lancé par systemd. Nginx relaie `/console` en WebSocket. - Déroulé : 1. Laravel demande à Nova l'adresse de la console série. 2. Laravel la range dans Redis sous un **jeton aléatoire**, pour 5 minutes. 3. Le navigateur reçoit uniquement ce jeton. 4. Le proxy lit Redis, ouvre le WebSocket vers nova-serialproxy et recopie les octets dans les deux sens. - La console série n'accepte qu'**une seule connexion** : c'est le proxy qui duplique le flux vers la vue du prof, en lecture seule. - Côté front : xterm.js, avec un onglet par VM du lab. **Critère :** dans le navigateur, l'élève tape `ip a` dans sa VM et voit le résultat, et un autre élève ne peut pas ouvrir cette console. ### Étape 10 : Liaison Laravel ↔ OpenStack et lab 08 - Client OpenStack dans Laravel, avec le client HTTP de Laravel ou `php-opencloud/openstack`, via les application credentials. - Modèle Heat du lab 08, d'après `paillasse-contenus/08-deux-lan-routeur/lab.yaml` : - réseaux de lab **sans DHCP**, avec `port_security_enabled: false` ; - **aucun routeur vers l'extérieur** : les VM des labs n'ont pas accès à Internet. - Jobs dans une file Redis : - `CreerStackLab` ; - `SupprimerStackLab` ; - expiration automatique des sessions. **Critère :** le bouton « Lancer le lab 08 » crée une stack `CREATE_COMPLETE`, et les terminaux de chacune de ses VM s'ouvrent. ### Étape 11 : Stock chaud et correction automatique **Stock chaud** - Une liste Redis de stacks prêtes par lab, remplie par un job. - Taille par défaut : calculée par toi selon la RAM disponible, et configurable. - Pré-remplissage automatique 10 minutes avant une séance. **Correction** - Un petit service `correcteur`, lancé par systemd, écoutant sur un socket Unix. - Il accepte **uniquement** : uuid d'instance, id du lab, id de la vérification. - Il exécute les vérifications **définies dans le YAML du lab** avec `guest-exec` de l'agent QEMU (par `virsh`, dans le conteneur `nova_libvirt`). - Jamais de commande libre venant du portail. - Laravel enregistre la note et le message pour l'élève. **Critère :** - un lab pris dans le stock s'ouvre en **moins de 5 s** ; - une configuration correcte donne « réussi » ; - une configuration incorrecte donne « échoué » avec un message utile. ### Étape 12 : Domaine, HTTPS et sécurité **Avant de commencer, arrête-toi et demande-moi le domaine** (je l'achète et je configure le DNS moi-même). - Certificat Let's Encrypt (certbot, plugin Nginx) et renouvellement automatique. - En-têtes de sécurité : HSTS, CSP adaptée à xterm.js, etc. - Grafana et Uptime Kuma derrière authentification, et la page de statut publique. - Vérifie à nouveau que seuls 22, 80 et 443 sont ouverts, et qu'aucune VM élève n'a d'IP publique ni d'accès sortant. **Critère :** le domaine affiche un certificat valide en HTTPS, et le parcours prof → élève → lab → correction fonctionne. ### Étape 13 : Tests de capacité et de résistance - Lance 20 VM `eleve.petit` en même temps. Mesure la RAM, le CPU et les temps de démarrage, puis écris `docs/capacite.md`. - Restaure une sauvegarde complète du portail sur une copie de test. - **Avec l'accord d'Adam**, redémarre le serveur : tout doit revenir seul (Kolla, portail, proxy, correcteur, supervision). **Critère :** - les chiffres de capacité sont écrits ; - la restauration est réussie ; - tout est revenu seul après le redémarrage. ### Étape 14 : Pages légales et RGPD Pages à créer : - `/legal` : mentions légales (Esquisse Formation ; le SIREN et l'adresse sont à compléter par Adam) ; - la politique de confidentialité ; - la page de statut. Job de purge : suppression 90 jours après l'archivage d'une classe. Interdits : - logo Marianne ou ministère ; - drapeau de l'UE ; - faux « logo RGPD » ; - KPI inventés ou faux clients. Formulations autorisées : « Hébergé en France » (c'est vrai), et « Conforme au RGPD » avec un lien vers la politique de confidentialité. **Critère :** pages en ligne, et un test Pest prouve que la purge fonctionne. ### Étape 15 : Visio de classe (seulement après validation des étapes 1 à 11) - Jitsi auto-hébergé, derrière Nginx. - Accès par **jeton JWT** émis par Laravel : seuls les élèves de la classe entrent, avec un nom non modifiable. - Le prof est seul modérateur. Tout le monde arrive micro et caméra coupés, et seul le prof donne la parole. - Main levée synchronisée avec celle de l'app, salle d'attente, chat désactivé par défaut. - Reprends exactement `design/apercu-visio.html` et `design/resources/views/visio.blade.php`. **Critère :** un prof et deux élèves fictifs rejoignent la visio ; un élève lève la main, le prof lui donne la parole, et un intrus sans jeton ne peut pas entrer. ### Étape 16 : Nettoyage Propose de retirer `/etc/sudoers.d/90-adam`. --- ## 4. Fin de mission Écris `docs/bilan.md`, avec : - ce qui fonctionne, avec la preuve de chaque critère ; - le temps réel passé par étape ; - la capacité mesurée (nombre d'élèves en même temps) ; - les dettes et les risques connus ; - **ce qu'Adam doit faire lui-même** : secrets à changer, comptes à créer, tests avec un prof, etc. ; - les prochaines étapes du bloc A : - self-healing et post-mortem ; - montée de version d'OpenStack ; - pilote avec une vraie classe. Commence par l'étape 1.