ajoute le self-healing des VM (sonde de santé, relance automatique, stock écarté) — bloc A, semaine 6

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-12 21:12:43 +02:00
parent b054888f5a
commit f2649d0f8b
21 changed files with 938 additions and 28 deletions

View file

@ -139,23 +139,44 @@ de test affiché en clair (credential supprimée aussitôt) ; un déploiement en
11. Sonde de supervision hors du serveur (petite VM ailleurs, ou service
externe) et nom DNS `turn.syllabuslab.fr` pour la visio.
## Prochaines étapes du bloc A
**Décision d'Adam (2026-10-12) : pas de montée de version d'OpenStack.** La
2025.2 reste en place (stable, préférée aux dernières versions) ; la semaine
7 du CLAUDE.md est donc écartée pour l'instant.
### 1. Self-healing et post-mortem (semaine 6 du CLAUDE.md)
## Mise à jour du 2026-10-12 : sauvegarde d'OpenStack et self-healing
- Détecter et relancer : VM figée (l'agent invité ne répond plus → `nova
reboot --hard`), stack du stock en erreur (supprimée et recréée : déjà en
partie fait par le ménage du stock), service arrêté (`Restart=on-failure` sur
toutes les unités maison, à vérifier une par une), conteneur Kolla arrêté.
- Une panne provoquée par cas (`kill -STOP` du QEMU d'une VM du stock, arrêt
du correcteur, arrêt de `nova_compute`), réparée sans intervention, chacune
suivie d'un post-mortem dans `docs/postmortems/`.
- Avant : sauvegarde de la base OpenStack, sinon une réparation ratée n'a pas
de filet.
Après la fin de la mission initiale, Adam a demandé de continuer le bloc A
(sans montée de version) :
### 2. Montée de version d'OpenStack (semaine 7)
- **Base d'OpenStack désormais sauvegardée** (Keystone, Nova, Neutron,
Glance, Heat, Placement) et testée chaque semaine comme celle du portail.
Dette retirée. Détails : `docs/journal.md`, `docs/runbooks/sauvegardes.md`.
- **Self-healing des VM fait** (semaine 6) : une VM muette (stock ou élève)
est détectée et réparée automatiquement par `labs:surveiller-vm`, chaque
minute. **Panne provoquée sur le serveur réel** (VM gelée, `virsh
suspend`) : détectée et redémarrée seule, sans intervention humaine.
Post-mortem : `docs/postmortems/2026-10-12-vm-figee-self-healing.md`.
Runbook : `docs/runbooks/self-healing.md`.
- Ne sont **pas** couverts par ce self-healing (hors sujet, déjà traités
ailleurs) : un service ou un conteneur Kolla arrêté (déjà
`Restart=always`/`unless-stopped`, vérifié le 2026-10-12 : 0 unité en
échec), et la panne du serveur lui-même (toujours sans sonde extérieure).
- Dette ajoutée : pas encore d'alerte Prometheus dédiée sur
`syllabus_vm_panne_generale` ni `syllabus_vm_irreparables`.
- Passer de la branche `stable/2025.2` à un tag (version figée), puis préparer
## Prochaines étapes du bloc A (restantes)
### Avant le pilote (semaine 8)
- TURN sur 443 pour la visio (réseaux scolaires qui bloquent l'UDP).
- Alerte Prometheus sur la panne générale des VM et les VM irréparables.
- Sonde de supervision hors du serveur.
- Les points « Adam doit faire lui-même » ci-dessus (clé age, CGU, test avec
un collègue prof…).
### Montée de version d'OpenStack (semaine 7) — écartée pour l'instant
Si Adam change d'avis : passer de la branche `stable/2025.2` à un tag (version figée), puis préparer
la série suivante. Attention : 2026.1 ne supporte plus Debian 12. Une montée
de version d'OpenStack passe donc d'abord par **Debian 13** (montée de
version de l'OS sur un serveur unique, à préparer avec le mode rescue).
@ -163,10 +184,11 @@ de test affiché en clair (credential supprimée aussitôt) ; un déploiement en
vérifications (`capacite.py` réduit, `parcours-lab.py`, `visio-e2e.mjs`),
retour arrière. Retour d'expérience dans le journal.
### 3. Pilote avec une vraie classe (semaine 8)
### Pilote avec une vraie classe (semaine 8)
- Prérequis : TURN 443, base OpenStack sauvegardée, clé age confirmée, sonde
- Prérequis : TURN 443, clé age confirmée, sonde
extérieure, mentions légales complètes, test avec un collègue (point 9).
La base OpenStack est désormais sauvegardée.
- Pendant : relever le temps d'ouverture réel (p95), la mémoire, la charge de
la visio, les corrections réussies et échouées, les mains levées.
- Après : retours des élèves et du prof, métriques dans `docs/capacite.md`,

View file

@ -29,3 +29,5 @@ retirer la ligne (et le noter dans le journal) quand c'est fait.
| **Visio : pas de relais TURN sur 443** | Les réseaux scolaires qui bloquent l'UDP n'auront ni son ni image (CLAUDE.md : tout par 443). Restructurer l'entrée HTTPS (aiguillage Nginx par nom) + coturn ; demande un nom DNS (turn.syllabuslab.fr). | Avant le pilote | Étape 15 |
| Visio : chat activé à chaud non vérifié | L'interrupteur « Chat des élèves » change la barre d'outils par `overwriteConfig` ; non vérifié en navigateur. Vue mobile de la maquette non reprise (la page élève est simplement adaptative). | Bloc B | Étape 15 |
| Image Jitsi figée sans procédure de mise à jour | `stable-11248` : comme pour Kuma, prévoir la montée de version par Ansible (variable `visio_version`) et rejouer `visio-e2e.mjs`. | Bloc B | Étape 15 |
| Self-healing : pas d'alerte Prometheus dédiée | `syllabus_vm_panne_generale` et `syllabus_vm_irreparables` sont publiées mais sans règle d'alerte ; seul le prof voit une VM irréparable dans la salle d'attente. | Avant le pilote | 2026-10-12 |
| Self-healing : ne couvre que les VM (pas le serveur, déjà en dette, ni les conteneurs/services, déjà `Restart=always`) | Documenté dans `docs/runbooks/self-healing.md`, pas une lacune à corriger — rappel pour ne pas le redemander. | — | 2026-10-12 |

View file

@ -716,3 +716,35 @@ projet paillasse dans la copie : 1
**Comprendre**
Avant, perdre le serveur voulait dire recréer à la main projet, compte de service, gabarit, quotas et image. Les fichiers de configuration (`/etc/kolla`, mots de passe compris) étaient déjà dans l'archive ; il manquait l'état, qui vit dans la base. Les fichiers des images Glance restent hors sauvegarde : ils se reconstruisent par script, et les VM d'élèves sont jetables par nature.
## 2026-10-12 — Bloc A, semaine 6 : self-healing des VM
**Fait**
- **Sonde de santé dans le correcteur** (Go) : nouvelle requête `{instance, sante: true}`, distincte de la correction (`{instance, lab, verification}`), refusée si les deux formes sont mélangées. Elle lit l'état libvirt (`virsh domstate`) et fait un `guest-ping` de l'agent QEMU — rien n'est exécuté dans la VM. Test Go `TestRequeteValide`.
- **`App\Services\Labs\SurveillanceVm`**, appelée chaque minute par `labs:surveiller-vm` (scheduler Laravel, déjà en place) : sonde toutes les VM du stock chaud et des sessions actives.
- **Stock** : une VM muette 2 sondes de suite → toute la stack est retirée du stock et supprimée ; `labs:remplir-stock` la remplace. Aucun élève ne reçoit une VM défaillante.
- **Élève** : une VM muette 3 sondes de suite → redémarrage forcé par Nova (`NovaClient::redemarrerForce`, nouveau ; démarrage simple si elle était éteinte), au plus 2 fois par heure. Au-delà : marquée « irréparable », visible du prof dans la salle d'attente.
- **Garde-fous** : délai de grâce de 3 min après la création (démarrage normal) ; aucune action si le correcteur est injoignable ; aucune action si *toutes* les VM sondées sont muettes à la fois (panne probable de l'hôte, pas des VM).
- Métriques Prometheus (`sante_vm.prom`, collecteur textfile) : VM sondées/muettes, irréparables, panne générale, compteur de réparations.
- 9 tests Pest (Redis simulé en mémoire, correcteur et Nova simulés).
- Déploiement : dossier `/var/lib/node_exporter/textfile` passé en groupe `www-data` (0775) pour que le scheduler (qui tourne sous ce compte) y écrive ; variable `LABS_SURVEILLANCE_METRIQUES` ajoutée au `.env`.
- Runbook `docs/runbooks/self-healing.md`.
**Panne provoquée sur le serveur réel** (post-mortem : `docs/postmortems/2026-10-12-vm-figee-self-healing.md`) : une VM figée (`virsh suspend`, comme un noyau planté) a été détectée et redémarrée automatiquement en 3 sondes, sans intervention humaine ; les deux autres VM de la même session n'ont jamais été touchées. Données de test supprimées (accord d'Adam, via « fonce, continue »).
**Problèmes rencontrés et solution**
1. **Déployé dans le mauvais ordre** : `portail.yml` (donc le scheduler avec `labs:surveiller-vm`) avant `correcteur.yml` (le correcteur recompilé avec la sonde). Pendant quelques minutes, le nouveau code a interrogé l'ancien correcteur, qui refusait la requête (`requete_invalide`) — remonté comme « VM muette » à tort, sans rapport avec une vraie panne. Corrigé en redéployant `correcteur.yml` immédiatement ; règle pour la suite : déployer le correcteur avant le portail quand les deux changent ensemble.
2. Le scheduler tourne en continu (cron, chaque minute) y compris pendant le test manuel : les appels à la main se sont mélangés aux passages automatiques, le seuil a été atteint plus vite que prévu. Pas un défaut, mais à prévoir pour un test chronométré (désactiver le cron le temps du test, ou ne lire que les métriques).
3. Pas d'alerte Prometheus dédiée encore écrite pour `syllabus_vm_panne_generale`/`syllabus_vm_irreparables` : noté en dette.
**Critère vérifié (semaine 6 du CLAUDE.md)** : panne provoquée → réparée seule → post-mortem écrit.
**Comprendre**
La vraie difficulté du self-healing n'est pas de détecter une panne, c'est de ne pas réagir n'importe comment à une panne qu'on a mal comprise : relancer en boucle une VM qui ne répond plus parce que l'hyperviseur entier est surchargé n'aiderait personne. D'où les deux garde-fous qui comptent le plus : un plafond de relances par heure (sinon une VM vraiment cassée consomme des cycles de redémarrage pour rien, autant prévenir le prof tout de suite), et le refus d'agir quand toutes les VM sont muettes à la fois (ce n'est plus une panne de VM, c'est une panne d'hôte, et ce mécanisme n'est pas fait pour ça).
**Suite**
Prochaines étapes du bloc A : TURN 443 pour la visio, sauvegarde OpenStack (fait le 2026-10-12), pilote avec une vraie classe. Pas de montée de version d'OpenStack (décision d'Adam : la 2025.2 reste en place).

View file

@ -0,0 +1,79 @@
# Post-mortem — Panne provoquée : VM figée, réparation automatique
**Date** : 2026-10-12. **Gravité** : aucune (panne provoquée volontairement,
sur le serveur réel, pour valider le self-healing du bloc A, semaine 6).
**Impact réel** : nul — une seule VM de test, aucun élève.
## Scénario
Une VM d'élève (3 machines du lab 08, prises du stock chaud et attribuées à
une session de test) est gelée avec `virsh suspend`, qui simule un noyau
planté ou un blocage matériel : le processeur virtuel s'arrête, l'agent
invité ne répond plus, mais la VM reste en mémoire (état libvirt `paused`).
## Déroulé observé
| Heure (locale) | Événement |
|---|---|
| 21:08:00 | VM `pc-a` mise en pause (`virsh suspend`) ; `routeur` et `pc-b` intactes |
| 21:08–21:10 | Sondes automatiques (`labs:surveiller-vm`, chaque minute) : `pc-a` muette, `routeur`/`pc-b` répondent |
| ≈ 21:10 | 3 sondes ratées de suite atteintes : Nova reçoit l'ordre de redémarrage forcé (`reboot --hard`) sur `pc-a` ; `routeur` et `pc-b` jamais touchées |
| + quelques secondes | `pc-a` repasse de `paused` à `running` ; la sonde suivante la trouve de nouveau vivante |
| — | `lab_session_machines.sante` = `relancee`, horodatée ; les deux autres machines restent `null` |
**Aucune intervention humaine entre la mise en pause et la réparation.**
## Preuve
```
# Avant : pc-a « running »
# Panne : virsh suspend → domstate « paused »
3 VM sondées, 1 muettes ; élèves : 1 VM relancée(s), 0 irréparable(s)
# Après : domstate « running »
3 VM sondées, 0 muettes ; élèves : 0 VM relancée(s), 0 irréparable(s)
sante = "relancee", sante_a = 2026-10-12 19:08:11
```
## Ce qui a bien fonctionné
- La sonde (correcteur) distingue bien une VM gelée (`paused`) d'une VM
éteinte (`shut off`) et d'une VM saine : seule `pc-a` a été touchée, les
deux autres machines de la même session n'ont jamais reçu d'action.
- Le redémarrage forcé a suffi : pas besoin de reconstruire la stack Heat ni
de perdre le travail de l'élève sur les deux autres machines.
- Le compteur de relances par heure a empêché une boucle : si la VM était
restée muette, elle serait passée « irréparable » après 2 tentatives, pas
redémarrée indéfiniment.
## Ce qui a été découvert pendant le test (pas un défaut, une leçon)
Le scheduler Laravel tourne **chaque minute en continu**, y compris pendant
qu'on teste à la main : les appels manuels de `labs:surveiller-vm` se sont
mélangés avec les passages automatiques du cron. Le seuil de 3 sondes
ratées a donc été atteint plus vite qu'en comptant seulement les commandes
tapées à la main. **À refaire** : pour un test reproductible au chronomètre,
désactiver temporairement le cron du scheduler (ou observer uniquement les
métriques, sans lancer la commande soi-même) — noté pour le prochain test
(montée en charge, panne générale).
Un piège trouvé **avant** ce test, pendant le déploiement : le correcteur
n'avait pas encore été recompilé avec la sonde quand le nouveau code
Laravel a commencé à l'appeler (cron déjà actif après `ansible-playbook
portail.yml`, avant `ansible-playbook correcteur.yml`). Les VM sont
apparues « muettes » quelques minutes à tort (erreur `requete_invalide` de
l'ancien binaire, pas une vraie panne). **Correction pour la suite** :
toujours déployer `correcteur.yml` avant `portail.yml` quand les deux
changent ensemble (le contraire de l'ordre suivi cette fois).
## Comprendre
Un self-healing qui ne distingue pas « cette VM précise est malade » de
« tout l'hôte est en panne » ferait plus de mal que de bien : redémarrer en
boucle 20 VM parce que l'hyperviseur lui-même est surchargé n'aiderait en
rien, et ajouterait de la charge au pire moment. La règle « toutes muettes à
la fois → aucune action » (vérifiée mais non provoquée pendant ce test,
seulement en test Pest) existe pour cette raison. L'autre leçon, plus
générale, rejoint celle de l'étape 7 : un service qui tourne en tâche de
fond (ici le scheduler, chaque minute) continue de tourner pendant qu'on
observe à la main — il faut en tenir compte en testant, pas le découvrir
après coup.

View file

@ -0,0 +1,93 @@
# Runbook — Self-healing des VM (bloc A, semaine 6)
## Principe
Chaque minute, le scheduler Laravel (compte `www-data`, déjà en place depuis
l'étape 10) exécute `php artisan labs:surveiller-vm`, implémenté par
`App\Services\Labs\SurveillanceVm`.
Pour chaque VM (stock chaud et sessions d'élèves actives), il demande au
service **correcteur** (sur l'hyperviseur) une **sonde de santé** :
état libvirt du domaine (`virsh domstate`) et `guest-ping` de l'agent invité
QEMU. Rien n'est exécuté dans la VM — c'est une requête distincte de la
correction d'un lab (`{instance, sante: true}` plutôt que
`{instance, lab, verification}`), refusée si elle porte les deux à la fois.
- **VM du stock muette 2 sondes de suite** (`stock:echecs` compteur Redis
`sante:muette:{instance}`) : sa stack est retirée du stock chaud et
supprimée. `labs:remplir-stock` la remplace à la prochaine minute. Aucun
élève ne reçoit jamais une VM défaillante.
- **VM d'élève muette 3 sondes de suite** : redémarrage forcé par Nova
(`reboot --hard`, ou démarrage si elle est éteinte), au plus **2 fois par
heure**. Au-delà, la VM est marquée **irréparable** : le prof la voit dans
la salle d'attente (« ne répond plus malgré deux redémarrages automatiques.
L'équipe technique est prévenue. »), et doit relancer le lab de cet élève
à la main.
## Garde-fous
- **Délai de grâce** de 3 minutes après la création d'une VM : elle démarre
encore, on ne la sonde pas (correspond au p95 de démarrage mesuré à
l'étape 13, 75 s, avec large marge).
- **Correcteur injoignable** : aucune action (on ne devine jamais l'état
d'une VM qu'on n'a pas pu sonder).
- **Panne générale** : si *toutes* les VM sondées sont muettes à la fois
(seuil : au moins 3 VM), c'est probablement l'hyperviseur qui est en
cause, pas les VM une par une — aucune relance n'est tentée (une rafale de
redémarrages ne répare pas un hôte en panne, et l'alourdirait). Couvert
par les alertes existantes (`DisqueSature`, `MemoireSaturee`,
`ServiceOpenStackArrete`), pas par ce mécanisme.
## Métriques (Prometheus, collecteur textfile)
Fichier `/var/lib/node_exporter/textfile/sante_vm.prom` (écrit par
`www-data`, lu par `node_exporter`) :
| Métrique | Sens |
|---|---|
| `syllabus_vm_surveillance_timestamp` | dernier passage |
| `syllabus_vm_sondees` / `syllabus_vm_muettes` | VM sondées / muettes à ce passage |
| `syllabus_vm_irreparables` | VM d'élèves toujours muettes après les relances |
| `syllabus_vm_panne_generale` | 1 si aucune action n'a été tentée (panne générale ou correcteur injoignable) |
| `syllabus_vm_reparations_total{type=...}` | compteur cumulé : `relance_session`, `retrait_stock`, `abandon` |
Alertes à ajouter avant le pilote (pas encore faites, voir `docs/dette.md`) :
`syllabus_vm_panne_generale == 1` pendant plus de 5 min, et
`syllabus_vm_irreparables > 0`.
## Lancer une vérification manuellement
```
ssh paillasse
cd /var/www/portail
sudo -u www-data HOME=/tmp php8.3 artisan labs:surveiller-vm
cat /var/lib/node_exporter/textfile/sante_vm.prom
```
## Provoquer une panne pour tester (ne jamais sur une vraie classe)
```
# 1. Prendre une VM réelle (une session de test, ou du stock) et la geler
# comme un noyau planté (le CPU virtuel s'arrête, l'agent ne répond plus) :
ssh paillasse "sudo docker exec nova_libvirt virsh suspend <uuid-instance>"
# 2. Observer (plusieurs minutes, ou plusieurs appels manuels) :
ssh paillasse "cd /var/www/portail && sudo -u www-data HOME=/tmp php8.3 artisan labs:surveiller-vm"
# 3. Vérifier le retour à l'état normal :
ssh paillasse "sudo docker exec nova_libvirt virsh domstate <uuid-instance>" # running
```
Compte-rendu du premier test réel : `docs/postmortems/2026-10-12-vm-figee-self-healing.md`.
## Limites connues
- Ne couvre pas la panne du serveur lui-même (un seul serveur, pas de sonde
extérieure — dette connue, `docs/dette.md`).
- Ne couvre pas un service Kolla arrêté ni un conteneur maison arrêté : ce
sont déjà `Restart=always`/`unless-stopped` côté systemd et Docker (voir
relevé du 2026-10-12 dans le journal), pas ce mécanisme.
- Une stack du stock chaud orpheline (ni muette, ni référencée) est déjà
traitée par `StockChaud::menage()` (étape 11) ; `SurveillanceVm` s'occupe
seulement des VM qui *répondent mal*, pas de celles qui n'existent plus
en Redis.