ajoute la liaison Laravel-OpenStack et le modèle Heat du lab 08

Un gabarit Heat généré depuis chaque lab.yaml, une stack par session de
lab (création à l'admission, suppression à l'expiration via des jobs
Redis et un worker systemd dédié). Passage du compte de service à une
authentification par mot de passe : Heat refuse de créer un trust à
partir d'un jeton d'application credential (voir ADR 0002).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-08 10:14:24 +02:00
parent 8742bde807
commit 67a754f733
34 changed files with 1095 additions and 91 deletions

View file

@ -0,0 +1,62 @@
# ADR 0002 — Authentification par mot de passe pour le compte de service, pas par application credential
- **Date** : 2026-10-08
- **Statut** : accepté
## Contexte
À l'étape 6, le compte de service `paillasse-service` (utilisé par le portail
Laravel pour piloter OpenStack) avait été équipé d'une *application
credential* plutôt que d'un mot de passe classique — plus simple à révoquer
sans toucher au mot de passe principal du compte, portée limitée à un projet.
Repris sans changement à l'étape 9 pour les appels Nova (console série).
À l'étape 10, en ajoutant un client Heat pour créer une stack par lab, la
création d'une stack de test a échoué avec `500 Internal Server Error`. Le
journal de `heat-engine` (`/var/lib/docker/volumes/kolla_logs/_data/heat/`,
rien dans `docker logs`) donnait la vraie cause :
```
keystoneauth1.exceptions.http.Forbidden: You are not authorized to perform
the requested action: Delegated tokens cannot manage trusts.
```
## Décision
**Le compte de service s'authentifie par mot de passe, pas par application
credential.**
Ce n'est pas un contournement : c'est une règle de sécurité Keystone, pas un
bug. Heat a besoin de créer un *trust* (Keystone délègue temporairement les
droits de l'appelant au moteur Heat, pour que celui-ci agisse en son nom
pendant toute la vie de la stack). Keystone refuse catégoriquement qu'un
jeton issu d'une application credential — un jeton déjà délégué une fois —
en délègue un second : ce serait une chaîne de délégation, un risque de
sécurité que Keystone élimine par construction, quelle que soit la
configuration.
Vérifié empiriquement avant de changer quoi que ce soit : mot de passe
régénéré pour `paillasse-service`, fichier `clouds-test-password.yaml`
temporaire avec `auth_type: password`, `openstack stack create --wait` →
`CREATE_COMPLETE`. Confirmé avec Adam (choix : « mot de passe classique pour
`paillasse-service` ») avant d'appliquer en dur.
Changements :
- `/etc/kolla/clouds-paillasse.yaml` : `auth_type: password` (mot de passe
généré une fois, stocké root:root 600 sur le serveur, jamais dans Git).
L'application credential `portail-syllabus` a été supprimée.
- `KeystoneAuth` (nouveau) : un seul point d'authentification Laravel,
partagé par `NovaClient` (étape 9, adapté) et `HeatClient` (étape 10).
- Un seul mécanisme d'authentification pour toutes les opérations OpenStack
du portail — pas un mécanisme par service OpenStack appelé.
## Conséquences
- Le mot de passe de `paillasse-service` est un secret plus sensible qu'une
application credential (pas de portée limitée à un projet par nature,
révocation = changement de mot de passe plutôt que suppression d'une
credential) : à tourner si jamais exposé, comme n'importe quel secret.
- Toute future intégration OpenStack qui utiliserait des trusts (Octavia,
Magnum, autres templates Heat) doit réutiliser `KeystoneAuth`, pas
réintroduire une application credential.

View file

@ -346,3 +346,35 @@ La vraie contrainte de cette étape, c'est qu'une console série ne supporte qu'
**Suite**
Étape 10 : liaison Laravel ↔ OpenStack (client HTTP ou `php-opencloud/openstack`) et modèle Heat du lab 08 — poser la vraie mécanique qu'on a seulement simulée à la main à cette étape (associer une VM à une session de lab).
## 2026-10-08 — Étape 10 : liaison Laravel ↔ OpenStack et modèle Heat du lab 08
**Fait**
- `GenerateurTemplateHeat` : traduit un `lab.yaml` (format paillasse-contenus : réseaux + machines, chaque machine listant les réseaux auxquels elle est reliée, dans l'ordre) en un vrai gabarit Heat (HOT) — un réseau/sous-réseau Neutron par réseau du lab (`port_security_enabled: false`, `enable_dhcp: false` : pas de pare-feu réseau ni de DHCP, les élèves configurent eux-mêmes, aucune route vers l'extérieur), un port + une instance Nova par machine, les ports attachés dans l'ordre déclaré (c'est cet ordre qui décide si une interface sera `ens3` ou `ens4` dans la VM), une sortie Heat par machine pour récupérer son identifiant une fois la stack créée.
- `HeatClient` (créer/lire/supprimer une stack) et `KeystoneAuth` (authentification Keystone partagée, jeton mis en cache 50 min) : voir [ADR 0002](adr/0002-mot-de-passe-pour-heat.md), la vraie découverte de cette étape.
- Une stack Heat par session de lab (par élève, par tentative), pas par classe : `CreerStackLab` (job Redis, déclenché à l'admission de l'élève) génère le gabarit, crée la stack, attend `CREATE_COMPLETE` (jusqu'à 2 minutes), enregistre une `LabSessionMachine` (nom, identifiant d'instance) par machine du lab. `SupprimerStackLab` (job Redis, déclenché à l'expiration) supprime la stack et ses VM, et classe la session en `expire` ou `terminee` selon le cas.
- `ExpirerSessionsLab` (commande Artisan `labs:expirer-sessions`), planifiée chaque minute via `Schedule::command()` dans `routes/console.php` + un cron système qui appelle `artisan schedule:run`.
- Les deux jobs ont besoin d'un worker : service systemd `portail-worker` (`queue:work`), déployé et redémarré par Ansible à chaque déploiement de code (sans ce redémarrage, un worker déjà lancé continue de faire tourner l'ancien code en mémoire).
- `labs/` : nouveau dossier à la racine du dépôt, un sous-dossier par lab, copié par Ansible vers `/opt/labs` sur le serveur (distinct de `paillasse-contenus/`, qui reste la source éditoriale ; `labs/` est la copie de travail que lit le portail). Lab 08 (deux réseaux locaux reliés par un routeur, 3 machines) copié en premier.
- Salle d'attente élève et console prof passées à plusieurs terminaux (un onglet par VM de la session), avec un préfixe de route supplémentaire (`{labSessionMachine}`) pour identifier quelle VM ouvrir.
**Problèmes rencontrés et solution**
1. **La vraie découverte de l'étape** : création de stack Heat refusée (`500`, trace dans `heat-engine.log` : « Delegated tokens cannot manage trusts »). Le compte de service du portail utilisait une application credential (choix de l'étape 6) ; Heat a besoin de créer un trust Keystone pour gérer le cycle de vie d'une stack, et Keystone interdit par construction à un jeton déjà délégué (une application credential en est un) d'en déléguer un second. Vérifié avec un compte à mot de passe classique avant de rien changer en dur ; confirmé avec Adam ; appliqué partout (voir ADR 0002). Un seul mécanisme d'authentification désormais pour tout ce que le portail demande à OpenStack.
2. `openstack user set --password` avec le cloud `kolla-admin` et `OS_PROJECT_NAME=paillasse` → `401` : même limitation que l'étape 6 (le compte `admin` n'a pas de rôle sur le projet `paillasse`, donc pas de jeton scopé à ce projet). Pas réinvestigué, contourné en utilisant directement le cloud `paillasse-service`.
3. `sudo -u www-data php artisan tinker` échouait (« Writing to directory /var/www/.config/psysh is not allowed ») : `www-data` n'a pas de `$HOME` inscriptible. Contourné avec `HOME=/tmp` à chaque appel.
**Critère vérifié (sur le serveur réel)**
- Classe, élève, séance de test créés ; admission de l'élève sur le lab 08 → `CreerStackLab` exécuté en 26 s → stack `CREATE_COMPLETE`, 3 VM créées. `ip link show` dans chaque VM confirme la topologie exacte du `lab.yaml` : le routeur a 2 interfaces (`ens3`, `ens4`), les deux postes (`pc-a`, `pc-b`) n'en ont qu'une chacun, sur des réseaux différents.
- Les 3 terminaux se sont ouverts côté élève, chacun relié à la bonne VM.
- Vérification allant au-delà du critère minimal : expiration forcée (`expire_a` mis dans le passé) → `labs:expirer-sessions` → `SupprimerStackLab` exécuté en 1 s → stack et 3 VM confirmées absentes (`openstack stack list` / `server list` vides) → session classée `expire`. Le cycle de vie complet (création, usage, expiration automatique) fonctionne de bout en bout sans intervention manuelle.
**Comprendre**
La leçon de cette étape dépasse Heat : dans Keystone, un jeton porte une histoire (« comment a-t-il été obtenu ? »), pas seulement une identité et des droits. Une application credential produit un jeton marqué comme délégué ; peu importe les droits du compte derrière, ce jeton ne pourra jamais en déléguer un second — c'est une protection contre les chaînes de délégation, pas un détail de configuration qu'on pourrait activer. Avant d'ajouter un service OpenStack qui gère un cycle de vie (Heat aujourd'hui ; Octavia, Magnum plus tard), il faut vérifier s'il a besoin d'un trust — et donc d'un compte à mot de passe, jamais d'une application credential.
**Suite**
Étape 11 : stock chaud de VM (files Redis) et correction automatique (agent invité QEMU).

49
docs/runbooks/labs.md Normal file
View file

@ -0,0 +1,49 @@
# Runbook — Labs et stacks Heat (étape 10)
## 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 |
| 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`) |
## 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.
## Déclencher l'expiration manuellement
```
ssh paillasse "cd /var/www/portail && sudo -u www-data HOME=/tmp php8.3 artisan labs:expirer-sessions"
```
## Inspecter une stack en cours 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}
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.