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:
parent
ca2f6f3b7a
commit
80b6c0722d
71 changed files with 2959 additions and 170 deletions
60
docs/adr/0003-stock-chaud-et-horizon.md
Normal file
60
docs/adr/0003-stock-chaud-et-horizon.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# ADR 0003 — Stock chaud dans Redis, Heat pour le ménage, files gérées par Horizon
|
||||
|
||||
- **Date** : 2026-10-08
|
||||
- **Statut** : accepté
|
||||
|
||||
## Contexte
|
||||
|
||||
Étape 11 : un lab pris dans le stock doit s'ouvrir en moins de 5 s, alors
|
||||
que Heat met 20 à 40 s à construire une stack. Il faut donc des stacks
|
||||
construites d'avance, données à un élève au moment où le prof l'admet, sans
|
||||
jamais donner la même stack à deux élèves, ni perdre une stack (une VM qui
|
||||
tourne pour rien coûte de la RAM, rare sur ce serveur de 32 Go).
|
||||
|
||||
## Décisions
|
||||
|
||||
1. **Une liste Redis par lab** (`stock:pretes:{lab}`) contient les stacks
|
||||
prêtes. La prise est un script Lua (`LPOP` + `ZADD stock:attribuees`)
|
||||
exécuté d'un bloc par Redis : une stack est toujours soit dans la liste,
|
||||
soit marquée « en cours d'attribution », jamais nulle part.
|
||||
2. **Redis fait foi pour ce qui est prêt ; Heat sert au ménage.** Chaque
|
||||
minute, `labs:remplir-stock` liste les stacks `stock-…` dans Heat et
|
||||
supprime celles que plus rien ne référence (ni la liste, ni une
|
||||
attribution, ni une session en base, ni une construction en cours), après
|
||||
10 min de marge. Une stack orpheline n'est jamais « récupérée » dans le
|
||||
stock : la supprimer est plus sûr que risquer de la donner deux fois.
|
||||
3. **Cible du stock** = taille par défaut (1 stack par lab) + élèves des
|
||||
séances qui commencent dans les 10 min ou déjà ouvertes et pas encore
|
||||
servis, dans la limite de `capacite_vm` (15 VM, voir journal de l'étape 11).
|
||||
4. **Fiabilité commune** (`FabriqueStack`) pour le stock et les sessions :
|
||||
3 essais, chaque essai porte son nom (`-e1`, `-e2`, `-e3`) et supprime sa
|
||||
stack ratée ; avant un essai, les restes des précédents sont supprimés.
|
||||
Disjoncteur sur le stock : 3 échecs définitifs de suite sur un lab →
|
||||
plus de commande pour lui pendant 30 min.
|
||||
5. **Laravel Horizon** remplace le `queue:work` unique de l'étape 10. Deux
|
||||
superviseurs à nombre fixe de processus : `sessions` (4) et `stock` (2).
|
||||
Un élève n'attend jamais derrière le remplissage du stock, et plusieurs
|
||||
stacks se construisent en parallèle (chaque job bloque un processus
|
||||
pendant qu'il attend Heat). Horizon était déjà prévu dans l'architecture
|
||||
du CLAUDE.md ; le besoin réel apparaît ici. Tableau de bord `/horizon`
|
||||
fermé à tous en production.
|
||||
|
||||
## Alternatives écartées
|
||||
|
||||
- **Une table MariaDB pour le stock** : possible, mais la prise atomique
|
||||
demande un verrou de ligne (`SELECT … FOR UPDATE SKIP LOCKED`) ; la liste
|
||||
Redis + Lua est plus simple, et le prompt de déploiement la demande.
|
||||
- **Récupérer les stacks orphelines au lieu de les supprimer** : économise
|
||||
une construction, mais ouvre une fenêtre où la même stack pourrait être
|
||||
dans la liste deux fois. Refusé.
|
||||
- **Plusieurs unités systemd `queue:work`** : faisable, mais sans métriques
|
||||
ni gestion propre de plusieurs files ; Horizon fait les deux.
|
||||
|
||||
## Conséquences
|
||||
|
||||
- Les VM du stock tournent en permanence (≈ 270 Mo réels chacune au repos).
|
||||
- Si Redis est vidé, le stock est perdu mais pas les VM : le ménage les
|
||||
supprime 10 min plus tard, et le stock se reconstruit.
|
||||
- `retry_after` de la file Redis (330 s) doit rester supérieur au timeout
|
||||
des superviseurs Horizon (300 s), lui-même supérieur au timeout des jobs
|
||||
(280 s).
|
||||
|
|
@ -378,3 +378,75 @@ La leçon de cette étape dépasse Heat : dans Keystone, un jeton porte une hist
|
|||
**Suite**
|
||||
|
||||
Étape 11 : stock chaud de VM (files Redis) et correction automatique (agent invité QEMU).
|
||||
|
||||
## 2026-10-08 — Entre les étapes 10 et 11 : questions d'Adam sur le compte de service
|
||||
|
||||
**Fait**
|
||||
|
||||
- `--unrestricted` testé sur le serveur réel : une application credential `--unrestricted` échoue avec la même erreur Keystone que la restreinte (`Delegated tokens cannot manage trusts`). Le mot de passe reste la seule option pour Heat. Ajouté à l'ADR 0002.
|
||||
- Contrôle du compte `paillasse-service` : seul rôle `member` sur `paillasse` (`reader` en découle, rôle impliqué), mot de passe de 32 octets aléatoires, `clouds-paillasse.yaml` en `600 root:root`. Il manquait le runbook de rotation : écrit (`docs/runbooks/compte-service-openstack.md`) et testé par une vraie rotation. Le classifieur de permissions a bloqué la fin de la rotation (écriture de secrets) : Adam a lancé lui-même le playbook et l'effacement du cache, puis j'ai vérifié l'authentification sans afficher de secret.
|
||||
- Défaut corrigé : un changement de `.env` par Ansible ne redémarrait pas le worker de file, qui aurait gardé l'ancien mot de passe en mémoire.
|
||||
|
||||
**Incident (de ma part)**
|
||||
|
||||
En créant l'application credential de test `--unrestricted`, j'ai demandé une sortie `-f json` qui a affiché son secret en clair, puis je l'ai écrit dans un fichier temporaire. La credential et le fichier ont été supprimés juste après le test : le secret est révoqué et sans valeur. Règle pour la suite : toute commande qui crée un secret doit l'envoyer directement dans un fichier `600` sur le serveur, sans jamais l'afficher, même pour un test.
|
||||
|
||||
## 2026-10-08 — Étape 11 : stock chaud et correction automatique
|
||||
|
||||
**Fait**
|
||||
|
||||
- **Fiabilité des créations** (demande d'Adam) : `FabriqueStack`, commune aux sessions et au stock. Un essai raté supprime sa stack et lève une exception ; Laravel relance le job 2 fois (après 15 puis 45 s), chaque essai sous son propre nom (`-e1`, `-e2`, `-e3`). Avant un essai, les restes des précédents sont supprimés (cas d'un processus tué). Après le 3ᵉ échec : session en `echec`, raison visible du prof, bouton « Réessayer ». L'admission est protégée contre le double clic par une mise à jour SQL conditionnelle.
|
||||
- **Stock chaud** : une liste Redis de stacks prêtes par lab, prise atomique par un script Lua, cible = 1 stack par lab + élèves des séances qui commencent dans 10 min ou déjà ouvertes, plafond de capacité, ménage des stacks orphelines par comparaison avec Heat, disjoncteur après 3 échecs. Séances planifiables (date et heure, heure de Paris convertie en UTC) : le stock monte au nombre d'élèves 10 min avant. Détails et alternatives : ADR 0003.
|
||||
- **Laravel Horizon 5.50** remplace le `queue:work` unique : superviseurs `sessions` (4 processus) et `stock` (2), tableau de bord fermé. Timeouts emboîtés : job 280 s < superviseur 300 s < `retry_after` 330 s (avant : 90 s, un job long aurait été exécuté deux fois).
|
||||
- **Correcteur** (Go, choix d'Adam) : service systemd root, groupe `www-data`, socket Unix `0660`, identité du client contrôlée par `SO_PEERCRED`. N'accepte que `{instance, lab, verification}` (champs en plus refusés), lit la commande dans le `lab.yaml`, vérifie dans les métadonnées libvirt que la VM appartient au projet `paillasse` et correspond à la machine visée, exécute par `guest-exec` via `docker exec nova_libvirt virsh` (10 s max). Ne renvoie jamais la sortie de la commande, seulement réussi/échoué et un code d'erreur. Installation de Go sortie dans un rôle `go` partagé avec le proxy console.
|
||||
- **Côté élève** : bouton « Vérifier mon travail », note, détail par vérification avec le `message_echec` du lab (ou un message technique si la VM ne répond pas). La page d'attente interroge l'état chaque seconde au lieu de se recharger toutes les 5 s. Côté prof : note dans la salle d'attente.
|
||||
- 7 nouveaux tests Pest (18 au total), 4 tests Go.
|
||||
|
||||
**Calcul de la capacité (`LABS_CAPACITE_VM` = 15)**
|
||||
|
||||
- Mesuré sans VM : 15,5 Go utilisés par Kolla et le portail sur 32 Go, 16,5 Go disponibles.
|
||||
- Mesuré avec les 3 VM d'une stack de stock au repos : 260 à 290 Mo réels par VM (sur 768 Mo alloués).
|
||||
- Pire cas retenu (chaque VM utilise toute sa RAM) : (16,5 Go − 2 Go de marge pour l'hôte) / (768 Mo + ≈ 200 Mo de surcoût QEMU) ≈ 15 VM. Au repos, le serveur en tiendrait trois fois plus : décider d'un surengagement (et de KSM) sera le sujet de l'étape 13, mesures en charge à l'appui. Stock par défaut : 1 stack par lab (3 VM pour le lab 08, 20 % du plafond).
|
||||
|
||||
**Commandes clés**
|
||||
|
||||
```
|
||||
cd ansible && script -q /dev/null ansible-playbook correcteur.yml -i inventory.ini # portail + correcteur
|
||||
ssh paillasse "cd /var/www/portail && sudo -u www-data HOME=/tmp php8.3 artisan labs:remplir-stock"
|
||||
ssh paillasse "journalctl -u correcteur -f"
|
||||
```
|
||||
|
||||
**Problèmes rencontrés et solution**
|
||||
|
||||
1. **La première touche frappée coupait la console** (défaut de l'étape 9) : `nova-serialproxy` ferme la connexion à la première trame WebSocket texte, et xterm.js envoie du texte. Le test de l'étape 9 envoyait du binaire et ne l'avait pas vu. Le proxy envoie désormais tout en binaire. Post-mortem : `docs/postmortems/2026-10-08-console-trames-texte.md`.
|
||||
2. **Aucun bouton pour qu'un élève rejoigne une séance** (défaut de l'étape 8, masqué aux étapes 9 et 10 où la session était créée en base) : ajouté au tableau de bord élève.
|
||||
3. **« Se déconnecter » renvoyait 419** : les deux formulaires du layout n'avaient pas de `@csrf`. Corrigé, et un test vérifie la présence du jeton (Laravel désactive le contrôle CSRF pendant les tests, il fallait tester le HTML).
|
||||
4. Une VM du stock démarrée depuis longtemps n'affiche rien sur sa console série : option `reveil` dans `console-reelle.js`, qui envoie un retour chariot à la connexion (côté élève seulement).
|
||||
5. Les séances n'avaient pas d'heure de début : colonne `prevue_a` et formulaire de planification.
|
||||
6. Larastan (PHPStan niveau 6, exigé par le CLAUDE.md) n'est pas installé depuis l'étape 8 : dette notée, pas traitée ici.
|
||||
7. Durée : l'étape a dépassé 1 h 30 (trois défauts d'étapes précédentes trouvés et corrigés en route), sans blocage.
|
||||
|
||||
**Critère vérifié (sur le serveur réel, `portail/tests/Reels/parcours-lab.py`, joué comme deux navigateurs)**
|
||||
|
||||
```
|
||||
1. Admission → invite de connexion : 4.06 s (OK, critère < 5 s)
|
||||
machines ouvertes : pc-a, routeur, pc-b
|
||||
2. Sans routage activé : « Pas encore réussi » 6/10 (vérification en 5.8 s)
|
||||
- Le routeur ne transmet pas les paquets : regarde net.ipv4.ip_forward.
|
||||
- PC-A ne joint pas PC-B. Vérifie les adresses, les passerelles et le routage, dans cet ordre.
|
||||
- La configuration du routeur disparaîtra au redémarrage (sysctl et fichier d'interfaces).
|
||||
3. Routage activé : « Réussi » 10/10 (vérification en 4.7 s)
|
||||
```
|
||||
|
||||
- Une seule mesure du temps d'ouverture, et pessimiste (le script récupère les 3 consoles l'une après l'autre, le navigateur en parallèle). Le p95 sur une série reste à établir à l'étape 13.
|
||||
- Échec provoqué (lab temporaire dont la stack ne peut pas se construire) : `heat-engine.log` montre `lab-session-5-e1`, `-e2`, `-e3` chacune `CREATE FAILED` puis `DELETE COMPLETE` ; session en `echec` 75 s après l'admission, aucune stack restante. Le stock du même lab : disjoncteur ouvert après les échecs, plus aucune commande.
|
||||
- Correcteur interrogé directement : vérification légitime exécutée ; machine non conforme, uuid hors projet, chemin `../../etc`, champ `commande` ajouté et vérification inconnue tous refusés ; un utilisateur autre que `www-data` ou root ne peut pas ouvrir le socket.
|
||||
- Données, lab temporaire et fichiers de test supprimés (accord d'Adam). État final : une stack de stock pour le lab 08, rien d'autre.
|
||||
|
||||
**Comprendre**
|
||||
|
||||
Le stock chaud transforme une attente de 30 s en une attente de 4 s en faisant le travail lent *avant* que quelqu'un en ait besoin — comme un restaurant qui prépare ses fonds de sauce le matin. La difficulté n'est pas de construire d'avance, c'est de ne jamais donner deux fois la même chose ni de perdre ce qu'on a construit : d'où le script Lua (Redis exécute « retirer de la liste et marquer comme en cours d'attribution » d'un seul bloc) et le ménage qui compare Redis à Heat. Côté correction, la règle de sécurité est simple et non négociable : le portail désigne une vérification par son nom, il n'envoie jamais de commande ; le correcteur, seul à toucher les VM, ne fait confiance qu'au `lab.yaml` et vérifie même que la VM désignée est bien celle du lab. Enfin, cette étape montre l'intérêt d'un test qui imite vraiment l'utilisateur : trois défauts des étapes précédentes, invisibles pour les tests « techniques », sont apparus dès qu'un script a joué le parcours comme un navigateur.
|
||||
|
||||
**Suite**
|
||||
|
||||
Étape 12 : domaine, HTTPS et sécurité. **Arrêt obligatoire avant de commencer** : Adam doit me donner le domaine (il l'achète et configure le DNS lui-même).
|
||||
|
|
|
|||
54
docs/postmortems/2026-10-08-console-trames-texte.md
Normal file
54
docs/postmortems/2026-10-08-console-trames-texte.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Post-mortem — la première touche frappée coupait la console de l'élève
|
||||
|
||||
- **Date** : 2026-10-08 (défaut introduit à l'étape 9, découvert à l'étape 11)
|
||||
- **Durée** : défaut latent depuis l'étape 9 ; corrigé dans l'heure suivant sa découverte
|
||||
- **Gravité** : élevée si elle avait atteint une classe — aucun élève n'aurait pu taper une seule commande dans un vrai navigateur. Impact réel nul : aucun utilisateur.
|
||||
- **Impact** : avec xterm.js, la première frappe de l'élève faisait fermer la connexion à la VM ; la console restait affichée mais muette.
|
||||
|
||||
## Résumé
|
||||
|
||||
`nova-serialproxy` (sous-protocole WebSocket `binary`) ferme sa connexion
|
||||
dès qu'il reçoit une trame **texte**. Or xterm.js, dans le navigateur, envoie
|
||||
les frappes en trames texte (`ws.send(chaîne)`), et le proxy Go les relayait
|
||||
vers l'amont avec leur type d'origine.
|
||||
|
||||
Le test de l'étape 9 (`proxy-console/test-manuel.py`) envoyait des trames
|
||||
**binaires** (`send_binary`) : il passait, et le critère « une élève tape
|
||||
`ip a` et voit le résultat » a été déclaré vérifié sans qu'aucun vrai
|
||||
navigateur n'ait tapé une touche.
|
||||
|
||||
## Chronologie
|
||||
|
||||
1. Étape 9 : proxy écrit et testé avec un script qui envoie du binaire. Critère validé.
|
||||
2. Étape 11 : le script de parcours (`portail/tests/Reels/parcours-lab.py`) imite xterm.js et envoie `"\r"` en trame texte pour réveiller la console : aucune invite `login:`.
|
||||
3. Journaux : `proxy-console` → « connexion amont fermée » 2 s après l'ouverture, à l'instant de la trame texte ; `nova-serialproxy` ne dit rien de plus.
|
||||
4. Diagnostic par comparaison directe (`/tmp/diag-trames.py`) : trame texte → rien puis connexion perdue.
|
||||
5. Correctif : `proxy-console/hub.go`, les frappes partent toujours en `websocket.BinaryMessage` vers l'amont. Redéployé ; texte et binaire font apparaître `login:`.
|
||||
|
||||
## Cause racine
|
||||
|
||||
Le test de l'étape 9 ne reproduisait pas le vrai client : il prenait un
|
||||
raccourci (binaire) que le navigateur ne prend pas. La différence entre
|
||||
« le chemin fonctionne » et « le chemin fonctionne avec le vrai client »
|
||||
n'était pas testée.
|
||||
|
||||
## Ce qui a bien fonctionné
|
||||
|
||||
- Écrire le test de l'étape 11 comme un vrai navigateur (connexion, CSRF, jeton de console, trames texte) a fait remonter le défaut avant tout pilote.
|
||||
- Les journaux du proxy, horodatés à la seconde, ont permis de relier la fermeture à la trame en quelques minutes.
|
||||
|
||||
## Actions correctives
|
||||
|
||||
- [x] Le proxy normalise : toute frappe part en binaire vers `nova-serialproxy`, quel que soit le client.
|
||||
- [x] `parcours-lab.py` envoie des trames texte, comme xterm.js : il protège désormais contre ce défaut.
|
||||
- [ ] Étape 12 : vérifier une fois le parcours dans un vrai navigateur (Playwright), la vérification visuelle de l'étape 8 n'ayant pas pu être faite.
|
||||
|
||||
## Comprendre
|
||||
|
||||
Un test qui passe ne prouve que ce qu'il teste vraiment. Le script de
|
||||
l'étape 9 parlait au proxy « comme un outil », pas « comme un navigateur » :
|
||||
il envoyait des octets bruts, alors qu'un navigateur envoie du texte. Les deux
|
||||
arrivent au même proxy, mais `nova-serialproxy` les traite différemment. La
|
||||
leçon générale : pour valider un critère utilisateur, le test doit imiter le
|
||||
vrai client jusque dans les détails qui paraissent anodins — ici, le type de
|
||||
trame WebSocket.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue