ajoute le stock chaud, la correction automatique et les nouveaux essais de création de stack

Stock de stacks prêtes par lab dans Redis (prise atomique en Lua, ménage
par comparaison avec Heat, disjoncteur), files gérées par Horizon, séances
planifiables. Correcteur Go sur l'hyperviseur : socket Unix, commandes lues
dans le lab.yaml uniquement, exécution par l'agent invité QEMU. Une création
de stack ratée est supprimée et réessayée deux fois, puis la session passe
en échec, visible du prof.

Corrige aussi trois défauts des étapes précédentes : la console se coupait
à la première frappe d'un navigateur (trames texte), l'élève ne pouvait pas
rejoindre une séance, la déconnexion n'avait pas de jeton CSRF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-08 21:52:16 +02:00
parent ca2f6f3b7a
commit 80b6c0722d
71 changed files with 2959 additions and 170 deletions

View file

@ -1,49 +1,93 @@
# Runbook — Labs et stacks Heat (étape 10)
# Runbook — Labs, stock chaud et correction (étapes 10 et 11)
## Où sont les choses
| Élément | Emplacement |
|---|---|
| Source éditoriale des labs | `paillasse-contenus/` (dépôt de contenu, pas de code) |
| Copie de travail lue par le portail | `labs/` à la racine de ce dépôt → synchronisée vers `/opt/labs` sur le serveur |
| Copie de travail lue par le portail et le correcteur | `labs/` à la racine de ce dépôt → synchronisée vers `/opt/labs` sur le serveur |
| Catalogue (table `labs`) | `php artisan labs:synchroniser`, lancé par Ansible après chaque synchronisation de `labs/` |
| Lecture d'un `lab.yaml` | `portail/app/Services/Labs/DefinitionLab.php` |
| Traduction lab → gabarit Heat | `portail/app/Services/OpenStack/GenerateurTemplateHeat.php` |
| Authentification OpenStack partagée | `portail/app/Services/OpenStack/KeystoneAuth.php` (mot de passe, pas application credential — voir [ADR 0002](../adr/0002-mot-de-passe-pour-heat.md)) |
| Appels Heat | `portail/app/Services/OpenStack/HeatClient.php` |
| Création de stack (à l'admission) | `portail/app/Jobs/CreerStackLab.php` |
| Suppression de stack (à l'expiration ou à la fin) | `portail/app/Jobs/SupprimerStackLab.php` |
| Expiration automatique | commande `labs:expirer-sessions` (`portail/app/Console/Commands/ExpirerSessionsLab.php`), planifiée chaque minute (`routes/console.php` + cron `www-data` sur le serveur) |
| Worker qui exécute les jobs | service systemd `portail-worker` (`queue:work`) |
| Construction d'une stack (3 essais) | `portail/app/Services/OpenStack/FabriqueStack.php` |
| Authentification OpenStack | `portail/app/Services/OpenStack/KeystoneAuth.php` (mot de passe — [ADR 0002](../adr/0002-mot-de-passe-pour-heat.md), rotation : [compte-service-openstack.md](compte-service-openstack.md)) |
| Stock chaud | `portail/app/Services/Labs/StockChaud.php`, job `CreerStackStock`, commande `labs:remplir-stock` chaque minute — [ADR 0003](../adr/0003-stock-chaud-et-horizon.md) |
| Admission (stock ou construction) | `SeanceController::admettre` → `CreerStackLab` si le stock est vide |
| Suppression et expiration | `SupprimerStackLab`, commande `labs:expirer-sessions` chaque minute |
| Files de jobs | Laravel Horizon, service systemd `portail-horizon` : superviseurs `sessions` (4 processus) et `stock` (2), `config/horizon.php` |
| Correcteur | service systemd `correcteur` (Go, `correcteur/`, binaire `/usr/local/sbin/correcteur`), socket `/run/correcteur/correcteur.sock` (`0660 root:www-data`) |
| Correction côté portail | `ClientCorrecteur`, `CorrigerSession`, table `corrections` |
| Tests du parcours réel | `portail/tests/Reels/` (préparation + parcours prof/élève) |
## États d'une session de lab
`en_attente` → (prof : Admettre) → `actif` tout de suite si le stock a une
stack, sinon `admis` puis `actif` quand `CreerStackLab` a fini → `expire` ou
`termine`. Après 3 essais ratés : `echec`, la raison est affichée au prof
qui peut cliquer « Réessayer » (retour à `admis`).
## Ajouter un nouveau lab
1. Le lab existe déjà en tant que contenu dans `paillasse-contenus/{id}/lab.yaml`, avec un format `reseaux: [{nom, cidr}]` et `machines: [{nom, image, flavor, reseaux: [...]}]` (l'ordre des réseaux d'une machine décide de l'ordre des interfaces : la première listée sera `ens3`, la suivante `ens4`, etc.).
2. Copier (ou faire relire) ce `lab.yaml` dans `labs/{id}/lab.yaml` de ce dépôt.
3. Vérifier que les images et flavors nommés existent déjà dans OpenStack (`openstack image list`, `openstack flavor list`).
4. Déployer (`ansible-playbook portail.yml`) : la tâche `04-labs.yml` synchronise `labs/` vers `/opt/labs` sur le serveur.
5. Tester : admettre un élève de test sur ce lab, vérifier que la stack passe en `CREATE_COMPLETE` et que chaque terminal s'ouvre.
1. Le lab existe dans `paillasse-contenus/{id}/lab.yaml`. L'ordre des réseaux d'une machine décide de l'ordre des interfaces (`ens3`, `ens4`…). Les vérifications dont l'id commence par `bonus-` ne comptent pas pour « réussi ».
2. Le copier dans `labs/{id}/lab.yaml` de ce dépôt.
3. Vérifier que les images et flavors existent (`openstack image list`, `openstack flavor list`) et que l'image a `hw_qemu_guest_agent=yes` (sinon le correcteur répond `agent_injoignable`).
4. Déployer : `cd ansible && script -q /dev/null ansible-playbook correcteur.yml -i inventory.ini` (portail + correcteur). Le lab entre au catalogue, et son stock se remplit dans la minute.
5. Tester le parcours : `portail/tests/Reels/` (voir l'en-tête des deux scripts), puis supprimer les données de test.
## Déclencher l'expiration manuellement
## Stock chaud
```
ssh paillasse "cd /var/www/portail && sudo -u www-data HOME=/tmp php8.3 artisan labs:expirer-sessions"
ssh paillasse "cd /var/www/portail && sudo -u www-data HOME=/tmp php8.3 artisan labs:remplir-stock"
```
## Inspecter une stack en cours sur le serveur
Affiche, par lab : cible, prêtes, en construction, commandées, retirées,
disjoncteur. Réglages (`.env` du portail, sinon valeurs de `config/labs.php`) :
- `LABS_CAPACITE_VM` (15) : plafond de VM `eleve.petit`, sessions et stock compris ;
- `LABS_STOCK_TAILLE` (1) : stacks prêtes par lab hors séance.
Disjoncteur ouvert (« oui ») : 3 échecs définitifs de suite sur ce lab. Lire
`storage/logs/laravel.log` (`CreerStackStock`), corriger la cause (image,
flavor, quota…), puis le réarmer sans attendre ses 30 min :
```
sudo -u www-data HOME=/tmp php8.3 artisan tinker --execute='Illuminate\Support\Facades\Redis::del("stock:echecs:{lab}");'
```
Vider le stock d'un lab (les stacks seront supprimées par le ménage) : passer
`LABS_STOCK_TAILLE=0` puis attendre une minute, ou supprimer la clé
`stock:pretes:{lab}` (le ménage supprime alors les stacks orphelines après 10 min).
## Correcteur
```
systemctl status correcteur
journalctl -u correcteur -f # une ligne par vérification : instance, lab, id, résultat, durée
```
Il n'accepte que `{"instance": uuid, "lab": id, "verification": id}`, refuse
tout champ en plus, et n'exécute que la commande du `lab.yaml`, en root dans
la VM, par `guest-exec` (10 s maximum). Avant d'exécuter, il vérifie dans les
métadonnées libvirt que la VM appartient au projet `paillasse` et porte le
nom de la machine visée. Codes d'erreur : `requete_invalide`, `lab_inconnu`,
`verification_inconnue`, `machine_non_conforme`, `agent_injoignable`,
`delai_depasse`.
## Inspecter les stacks sur le serveur
```
sudo -i
export PATH=/opt/kolla-venv/bin:$PATH
export OS_CLIENT_CONFIG_FILE=/etc/kolla/clouds-paillasse.yaml
export OS_CLOUD=paillasse-service
openstack stack list
openstack stack show lab-session-{id}
export OS_CLIENT_CONFIG_FILE=/etc/kolla/clouds-paillasse.yaml OS_CLOUD=paillasse-service
openstack stack list # lab-session-{id}-e{n} : sessions ; stock-{lab}-{xxxxxx}-e{n} : stock
openstack server list
```
## Pièges rencontrés
- **Heat refuse de créer une stack avec une application credential** (`Delegated tokens cannot manage trusts`) : Heat a besoin de créer un trust Keystone, qu'un jeton déjà délégué ne peut jamais déléguer à son tour. Le compte `paillasse-service` s'authentifie par mot de passe pour cette raison — voir [ADR 0002](../adr/0002-mot-de-passe-pour-heat.md). Ne jamais revenir à une application credential pour ce compte.
- **Après un déploiement de code, le worker doit redémarrer** (`notify: redémarrer le worker de file` sur la tâche de synchronisation) : un `queue:work` déjà lancé garde l'ancien code en mémoire tant qu'il n'est pas relancé.
- `sudo -u www-data php artisan ...` échoue sans `HOME=/tmp` (« Writing to directory /var/www/.config/psysh is not allowed ») : `www-data` n'a pas de `$HOME` inscriptible sur ce serveur.
- Une stack Heat appartient à une session de lab (un élève, une tentative), jamais à une classe : ne pas chercher à réutiliser une stack entre deux élèves ou deux tentatives.
- **Heat refuse une application credential** (`Delegated tokens cannot manage trusts`), même `--unrestricted` : voir [ADR 0002](../adr/0002-mot-de-passe-pour-heat.md).
- **Horizon garde le code et la configuration en mémoire** : le rôle Ansible le redémarre après chaque synchronisation de code ou changement de `.env`.
- **Timeouts emboîtés** : job (280 s) < superviseur Horizon (300 s) < `REDIS_QUEUE_RETRY_AFTER` (330 s). Sinon Redis redonne un job encore en cours à un autre processus, et deux stacks sont construites.
- **Console muette après la première touche** : `nova-serialproxy` ferme la connexion à la première trame WebSocket texte. Le proxy Go envoie tout en binaire — voir [post-mortem du 2026-10-08](../postmortems/2026-10-08-console-trames-texte.md).
- `sudo -u www-data php artisan ...` exige `HOME=/tmp` (pas de `$HOME` inscriptible pour `www-data`).
- Une stack appartient à une session (un élève, une tentative) ; une stack du stock n'y retourne jamais après avoir été donnée.