construit l'image paillasse-debian, crée le projet OpenStack et teste la console série

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-07 16:14:06 +02:00
parent 21c9de6d10
commit 44384847e5
8 changed files with 331 additions and 1 deletions

4
.gitignore vendored
View file

@ -1,8 +1,10 @@
.env .env
.DS_Store .DS_Store
# Secrets Kolla : ne vivent que sur le serveur (/etc/kolla) # Secrets Kolla / OpenStack : ne vivent que sur le serveur (/etc/kolla)
passwords.yml passwords.yml
clouds.yaml clouds.yaml
clouds-*.yaml
admin-openrc.sh admin-openrc.sh
*-openrc.sh *-openrc.sh
*application-credential*.json

10
ansible/images.yml Normal file
View file

@ -0,0 +1,10 @@
---
# Étape 6 du déploiement Syllabus : outillage de construction d'images.
# Ne construit pas l'image elle-même (longue, a besoin d'Internet) : voir
# docs/runbooks/images.md pour lancer construire-paillasse-debian.sh.
- name: Outillage de construction d'images
hosts: syllabus
become: true
gather_facts: true
roles:
- images

View file

@ -0,0 +1,33 @@
---
- name: installer virt-customize et les outils de construction d'image
ansible.builtin.apt:
name:
- libguestfs-tools
- qemu-utils
state: present
update_cache: true
cache_valid_time: 3600
- name: créer le dossier de construction d'image
ansible.builtin.file:
path: /opt/construction-image
state: directory
owner: root
group: root
mode: "0750"
- name: déployer le script de construction de paillasse-debian
ansible.builtin.copy:
src: "{{ playbook_dir }}/../openstack/images/construire-paillasse-debian.sh"
dest: /opt/construction-image/construire-paillasse-debian.sh
owner: root
group: root
mode: "0750"
- name: déployer le jeu de données du lab 07
ansible.builtin.copy:
src: "{{ playbook_dir }}/../paillasse-contenus/07-sql/lycee.sql"
dest: /opt/construction-image/lycee.sql
owner: root
group: root
mode: "0640"

View file

@ -196,3 +196,41 @@ Kolla-Ansible déploie OpenStack sous forme de conteneurs Docker : chaque servic
**Suite** **Suite**
Étape 6 : projet `paillasse`, utilisateur de service avec application credentials, image `paillasse-debian` (virt-customize), flavor `eleve.petit`, première VM et console série par WebSocket. Étape 6 : projet `paillasse`, utilisateur de service avec application credentials, image `paillasse-debian` (virt-customize), flavor `eleve.petit`, première VM et console série par WebSocket.
## 2026-10-07 — Étape 6 : première VM et console série
**Fait**
- Rôle Ansible `images` + playbook `ansible/images.yml` : installe `libguestfs-tools`, dépose `openstack/images/construire-paillasse-debian.sh` et `paillasse-contenus/07-sql/lycee.sql` sur le serveur.
- `paillasse-contenus/07-sql/lycee.sql` créé : tables `classes`, `eleves`, `notes` avec données d'exemple. Volontairement sans la vue `moyennes`, la table `clubs` ni l'utilisateur `prof` : c'est le travail demandé à l'élève.
- Projet OpenStack `paillasse`, utilisateur de service `paillasse-service` (mot de passe généré aléatoirement, jamais affiché, jeté après usage), *application credential* `portail-syllabus` (rôle `member`) enregistrée dans `/etc/kolla/clouds-paillasse.yaml` (`600 root:root`, jamais dans Git — `.gitignore` élargi à `clouds-*.yaml` et `*application-credential*.json`).
- Image `paillasse-debian` construite avec `virt-customize` depuis `debian-12-genericcloud-amd64.qcow2` officiel (somme SHA512 vérifiée) : `qemu-guest-agent`, getty sur `ttyS0`, utilisateur `eleve` (sudo, mot de passe `eleve`), outils réseau, `nginx`/`mariadb-server`/`frr` installés mais désactivés, `/opt/labs/07/lycee.sql`, `/opt/paquets/tree_*.deb`, **sans cloud-init** (retiré : inutile et seulement source de lenteur sur des réseaux de lab sans DHCP ni métadonnées). Importée dans Glance avec `hw_qemu_guest_agent=yes`, rendue `public` (sinon invisible aux autres projets).
- Flavor `eleve.petit` : 1 vCPU, 768 Mo, 5 Go.
- VM de test `vm-test-console` démarrée sur un réseau de test (`reseau-test`, sans DHCP, port security désactivée — même style que les futurs réseaux de lab), en ~25 s.
- Console série récupérée via `openstack console url show --serial`, testée avec un petit script Python (`openstack/images/test-console-serie.py`, module `websocket-client`) : connexion `eleve`/`eleve`, commande exécutée.
**Écart avec `paillasse-contenus/README.md`**
Le README liste `tree` parmi les outils préinstallés, mais le lab 02 demande explicitement à l'élève de l'installer lui-même depuis `/opt/paquets/` (sans Internet dans les VM). J'ai tranché en faveur du lab, qui a une vérification automatique dessus : `tree` n'est **pas** préinstallé, son `.deb` est dans `/opt/paquets/`. À signaler à Adam : `README.md` est à corriger (retirer `tree` de la liste des outils), ou le lab à revoir si c'était voulu.
**Problèmes rencontrés et solution**
1. `openstack user create --password-prompt` : `No terminal detected`, aucun TTY dans ce contexte. Remplacé par `--password` avec un mot de passe généré par `openssl rand`, jamais affiché ni réutilisé (l'authentification définitive passe par l'application credential).
2. `openstack application credential create -f json` : `KeyError: 'id'`. Le client renvoie des clés en majuscule (`ID`, `Secret`...), pas en minuscule. Corrigé.
3. `openstack server create --project paillasse` : `--project` n'existe pas sur cette sous-commande (la portée vient du token). Résolu en utilisant directement le cloud `paillasse-service` (application credential), déjà scopé au bon projet.
4. Image invisible pour `paillasse-service` : créée avec visibilité `shared` (nécessite une acceptation explicite par projet). Passée en `public`, cohérent avec un usage par tous les établissements plus tard.
5. Console série : la première tentative du script de test a échoué (`socket is already closed`) en envoyant des trames texte — nova-serialproxy attend des trames **binaires** (sous-protocole `binary`). Corrigé avec `ws.send_binary()`.
6. La console série est un flux continu, pas une session par connexion : un essai de login interrompu laisse le prompt au milieu d'une saisie pour la connexion suivante. Un script de test doit lire l'état actuel avant d'envoyer quoi que ce soit, pas supposer qu'il part d'un prompt frais.
**Critère vérifié**
- VM créée et `ACTIVE` en ~25 s.
- Script connecté au WebSocket de la console série : prompt `localhost login:` affiché, connexion réussie, commande `whoami && hostname && echo ---OK---` exécutée avec la sortie attendue (`eleve`, `localhost`, `---OK---`).
**Comprendre**
Une image de VM, ce n'est pas juste un système d'exploitation : c'est tout ce dont les labs ont besoin pour fonctionner sans accès Internet, préparé à l'avance une fois pour toutes. `virt-customize` permet de modifier une image disque sans jamais la démarrer, en manipulant directement son système de fichiers. Les *application credentials* sont le bon réflexe pour un compte de service : contrairement à un mot de passe, elles sont limitées à un projet et un rôle précis, et peuvent être révoquées sans toucher au compte lui-même. La console série, elle, prouve qu'on peut atteindre une VM même si son réseau est complètement cassé ou absent — exactement ce que demande CLAUDE.md pour l'espace de lab : « elle doit marcher même si l'élève casse le réseau de sa VM ».
**Suite**
Étape 7 : supervision (Prometheus, Grafana, Uptime Kuma) et alertes, accessibles par tunnel SSH en attendant un nom de domaine.

71
docs/runbooks/images.md Normal file
View file

@ -0,0 +1,71 @@
# Runbook — Image `paillasse-debian`
## Construire (ou reconstruire) l'image
Sur le serveur, en root. Le script ne supprime jamais une image existante
tout seul (règle 3) : il s'arrête si `paillasse-debian` existe déjà.
```
ssh paillasse
sudo bash /opt/construction-image/construire-paillasse-debian.sh
```
Déployé par `ansible/images.yml` (installe `libguestfs-tools`, copie le
script et `paillasse-contenus/07-sql/lycee.sql` vers
`/opt/construction-image/` sur le serveur) :
```
cd ansible && script -q /dev/null ansible-playbook images.yml -i inventory.ini
```
Durée : quelques minutes (téléchargement ~700 Mo + personnalisation).
## Ce que contient l'image
Depuis `debian-12-genericcloud-amd64.qcow2` officiel (somme SHA512 vérifiée) :
- `sudo`, `qemu-guest-agent` (correction automatique), getty actif sur `ttyS0` (console série).
- Utilisateur `eleve` (groupe `sudo`, mot de passe `eleve`).
- Outils réseau : `iproute2`, `iputils-ping`, `traceroute`, `tcpdump`, `dnsutils`, `curl`, `netcat-openbsd`.
- `openssl`, `nano`, `vim`, `htop`.
- `nginx`, `mariadb-server`, `frr` installés mais **désactivés** (activés par l'étape `preparation` de certains labs, ex. lab 07).
- `/opt/labs/07/lycee.sql` : jeu de données du lab 07 (voir `paillasse-contenus/07-sql/`).
- `/opt/paquets/tree_*.deb` : paquet `tree` pour le lab 02, **volontairement non installé** (le lab demande à l'élève de l'installer lui-même ; voir l'écart avec `paillasse-contenus/README.md` noté dans le journal du 2026-10-07).
- **Pas de cloud-init** (retiré) : les réseaux de lab n'ont ni DHCP ni service de métadonnées joignable, cloud-init n'attendrait que pour rien à chaque démarrage. La correction automatique passe par l'agent QEMU, pas par cloud-init/user-data.
Propriétés Glance : `hw_qemu_guest_agent=yes`, `hw_rng_model=virtio`, `os_type=linux`, visibilité `public`.
## Se connecter à une VM par la console série
```
ssh paillasse
sudo -i
export PATH=/opt/kolla-venv/bin:$PATH
export OS_CLIENT_CONFIG_FILE=/etc/kolla/clouds-paillasse.yaml # ou clouds.yaml + OS_CLOUD=kolla-admin
export OS_CLOUD=paillasse-service
openstack console url show --serial <nom-ou-id-de-la-vm>
```
Donne une URL `ws://10.10.0.1:6083/?token=...`, **jointe uniquement depuis le
réseau interne** (pare-feu : non exposée sur `eno1`). Pour un test manuel
depuis le serveur :
```
/usr/bin/python3 /opt/construction-image/test-console-serie.py "<url>" "une-commande"
```
(nécessite le module Python `websocket-client`, déjà présent sur le serveur).
Le script attend `login:`, envoie `eleve`, attend `Password:`, envoie `eleve`,
puis tape la commande. **La console série est un flux continu** : si un essai
précédent a laissé le prompt au milieu d'une saisie (ex. en attente d'un mot
de passe), le script suivant hérite de cet état — relire d'abord l'état
actuel avant d'envoyer quoi que ce soit, comme fait le script.
Plus tard (étape 9), c'est le proxy Go qui jouera ce rôle côté portail, avec
xterm.js côté navigateur, et un jeton à usage unique rangé dans Redis au lieu
d'un accès direct à `10.10.0.1:6083`.
## Vérifié le 2026-10-07
- VM `vm-test-console` (réseau de test `reseau-test`, 192.168.100.0/24, sans DHCP) démarrée en ~25 s.
- Console série : prompt `localhost login:` affiché, connexion `eleve`/`eleve` réussie, commande `whoami && hostname && echo ---OK---` exécutée avec la sortie attendue.

View file

@ -0,0 +1,81 @@
#!/bin/bash
# Construit l'image Glance `paillasse-debian`, à lancer sur le serveur Syllabus
# (a besoin d'Internet pour télécharger l'image cloud officielle et les
# paquets, et de /dev/kvm pour que virt-customize aille vite).
#
# Usage : sudo bash construire-paillasse-debian.sh
#
# Ne supprime jamais une image existante toute seule (règle 3 : suppression =
# accord explicite d'Adam) : s'arrête si `paillasse-debian` existe déjà.
set -euo pipefail
DOSSIER_SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# lycee.sql est copié ici par Ansible (ansible/roles/images/), à côté du
# script, pour ne pas avoir besoin d'un clone complet du dépôt sur le serveur.
LYCEE_SQL="$DOSSIER_SCRIPT/lycee.sql"
URL_BASE="https://cloud.debian.org/images/cloud/bookworm/latest"
IMAGE_BASE="debian-12-genericcloud-amd64.qcow2"
export OS_CLIENT_CONFIG_FILE=/etc/kolla/clouds.yaml
export OS_CLOUD=kolla-admin
export PATH=/opt/kolla-venv/bin:$PATH
if openstack image show paillasse-debian >/dev/null 2>&1; then
echo "L'image « paillasse-debian » existe déjà dans Glance." >&2
echo "Supprime-la toi-même si tu veux la reconstruire (openstack image delete paillasse-debian) : Claude ne le fait pas automatiquement." >&2
exit 1
fi
TRAVAIL=$(mktemp -d)
trap 'rm -rf "$TRAVAIL"' EXIT
echo "== Téléchargement de l'image cloud Debian 12 officielle =="
curl -fsSL -o "$TRAVAIL/SHA512SUMS" "$URL_BASE/SHA512SUMS"
curl -fsSL -o "$TRAVAIL/$IMAGE_BASE" "$URL_BASE/$IMAGE_BASE"
echo "== Vérification de la somme SHA512 =="
(cd "$TRAVAIL" && grep " $IMAGE_BASE\$" SHA512SUMS | sha512sum -c -)
IMAGE_TRAVAIL="$TRAVAIL/paillasse-debian.qcow2"
cp "$TRAVAIL/$IMAGE_BASE" "$IMAGE_TRAVAIL"
echo "== Préparation des fichiers à copier dans l'image =="
mkdir -p "$TRAVAIL/copie/labs/07" "$TRAVAIL/copie/paquets"
cp "$LYCEE_SQL" "$TRAVAIL/copie/labs/07/lycee.sql"
echo "== Téléchargement du paquet tree (lab 02 : installation hors ligne) =="
apt-get update -qq
(cd "$TRAVAIL/copie/paquets" && apt-get download tree)
echo "== Personnalisation avec virt-customize =="
# Remarque sur `tree` : volontairement absent de --install. Le lab 02 demande
# à l'élève de l'installer lui-même depuis /opt/paquets/ ; il serait sans
# intérêt pédagogique s'il était déjà présent (contrairement à ce que
# suggérait une première lecture de paillasse-contenus/README.md).
#
# cloud-init est retiré : les réseaux de lab n'ont ni DHCP ni service de
# métadonnées joignable (pas de routeur), donc cloud-init ne ferait
# qu'attendre inutilement à chaque démarrage. La correction automatique passe
# par l'agent QEMU (guest-exec), pas par cloud-init/user-data.
virt-customize -a "$IMAGE_TRAVAIL" \
--install sudo,qemu-guest-agent,iproute2,iputils-ping,traceroute,tcpdump,dnsutils,curl,netcat-openbsd,openssl,nano,vim,htop,nginx,mariadb-server,frr \
--run-command 'useradd -m -s /bin/bash -G sudo eleve' \
--password 'eleve:password:eleve' \
--run-command 'systemctl enable serial-getty@ttyS0.service' \
--run-command 'systemctl disable nginx mariadb frr' \
--run-command 'apt-get purge -y cloud-init && rm -rf /etc/cloud /var/lib/cloud' \
--run-command 'mkdir -p /opt/labs/07' \
--copy-in "$TRAVAIL/copie/labs/07/lycee.sql:/opt/labs/07" \
--copy-in "$TRAVAIL/copie/paquets:/opt"
echo "== Import dans Glance =="
openstack image create "paillasse-debian" \
--disk-format qcow2 --container-format bare \
--property hw_qemu_guest_agent=yes \
--property hw_rng_model=virtio \
--property os_type=linux \
--file "$IMAGE_TRAVAIL"
openstack image show paillasse-debian -c id -c status -c size -c properties
echo "== Terminé =="

View file

@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Test manuel de la console série Nova (étape 6 du déploiement).
Usage : python3 test-console-serie.py <url-ws> <commande-a-taper>
"""
import sys
import time
import websocket
url = sys.argv[1]
commande = sys.argv[2] if len(sys.argv) > 2 else "echo bonjour-syllabus"
ws = websocket.create_connection(url, subprotocols=["binary"], timeout=10)
def lire(duree=3):
fin = time.time() + duree
sortie = b""
while time.time() < fin:
ws.settimeout(max(0.2, fin - time.time()))
try:
morceau = ws.recv()
except websocket.WebSocketTimeoutException:
break
except websocket.WebSocketConnectionClosedException:
print("[connexion fermée par le serveur]")
break
if isinstance(morceau, str):
morceau = morceau.encode()
if not morceau:
break
sortie += morceau
return sortie
print("=== lecture initiale (vide la file, y compris un essai précédent) ===")
sys.stdout.buffer.write(lire(5))
sys.stdout.flush()
print("\n=== connexion : eleve ===")
ws.send_binary(b"eleve\r")
sys.stdout.buffer.write(lire(2))
sys.stdout.flush()
print("\n=== mot de passe ===")
ws.send_binary(b"eleve\r")
sys.stdout.buffer.write(lire(2))
sys.stdout.flush()
print(f"\n=== envoi de la commande : {commande} ===")
ws.send_binary((commande + "\r").encode())
sys.stdout.buffer.write(lire(3))
sys.stdout.flush()
ws.close()
print("\n=== fin ===")

View file

@ -0,0 +1,38 @@
-- Jeu de données de départ du lab 07-sql. Chargé dans la VM `bdd` par l'étape
-- `preparation` du lab : `mysql < /opt/labs/07/lycee.sql`.
-- Ne contient volontairement ni la vue `moyennes`, ni la table `clubs`, ni
-- l'utilisateur `prof` : c'est le travail demandé à l'élève.
CREATE DATABASE IF NOT EXISTS lycee;
USE lycee;
CREATE TABLE classes (
id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(20) NOT NULL
);
CREATE TABLE eleves (
id INT AUTO_INCREMENT PRIMARY KEY,
pseudonyme VARCHAR(30) NOT NULL,
classe_id INT NOT NULL,
FOREIGN KEY (classe_id) REFERENCES classes(id)
);
CREATE TABLE notes (
id INT AUTO_INCREMENT PRIMARY KEY,
eleve_id INT NOT NULL,
matiere VARCHAR(30) NOT NULL,
note DECIMAL(4,2) NOT NULL,
FOREIGN KEY (eleve_id) REFERENCES eleves(id)
);
INSERT INTO classes (nom) VALUES ('Tle NSI'), ('BTS SIO 1re année');
INSERT INTO eleves (pseudonyme, classe_id) VALUES
('faucon', 1), ('loutre', 1), ('renard', 2), ('heron', 2);
INSERT INTO notes (eleve_id, matiere, note) VALUES
(1, 'NSI', 14.5), (1, 'Maths', 12.0), (1, 'Anglais', 16.0),
(2, 'NSI', 9.0), (2, 'Maths', 11.0), (2, 'Anglais', 13.0),
(3, 'SISR', 15.0), (3, 'Maths', 10.0),
(4, 'SISR', 18.0), (4, 'Maths', 14.5);