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:
parent
8742bde807
commit
67a754f733
34 changed files with 1095 additions and 91 deletions
62
docs/adr/0002-mot-de-passe-pour-heat.md
Normal file
62
docs/adr/0002-mot-de-passe-pour-heat.md
Normal 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.
|
||||
|
|
@ -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
49
docs/runbooks/labs.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue