From 44384847e538684e175afed5d7afa9de6920f4de Mon Sep 17 00:00:00 2001 From: Adam Belkacemi Date: Wed, 7 Oct 2026 16:14:06 +0200 Subject: [PATCH] =?UTF-8?q?construit=20l'image=20paillasse-debian,=20cr?= =?UTF-8?q?=C3=A9e=20le=20projet=20OpenStack=20et=20teste=20la=20console?= =?UTF-8?q?=20s=C3=A9rie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 --- .gitignore | 4 +- ansible/images.yml | 10 +++ ansible/roles/images/tasks/main.yml | 33 ++++++++ docs/journal.md | 38 +++++++++ docs/runbooks/images.md | 71 ++++++++++++++++ .../images/construire-paillasse-debian.sh | 81 +++++++++++++++++++ openstack/images/test-console-serie.py | 57 +++++++++++++ paillasse-contenus/07-sql/lycee.sql | 38 +++++++++ 8 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 ansible/images.yml create mode 100644 ansible/roles/images/tasks/main.yml create mode 100644 docs/runbooks/images.md create mode 100644 openstack/images/construire-paillasse-debian.sh create mode 100644 openstack/images/test-console-serie.py create mode 100644 paillasse-contenus/07-sql/lycee.sql diff --git a/.gitignore b/.gitignore index 2ef7bb1..8873531 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ .env .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 clouds.yaml +clouds-*.yaml admin-openrc.sh *-openrc.sh +*application-credential*.json diff --git a/ansible/images.yml b/ansible/images.yml new file mode 100644 index 0000000..5c5cf45 --- /dev/null +++ b/ansible/images.yml @@ -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 diff --git a/ansible/roles/images/tasks/main.yml b/ansible/roles/images/tasks/main.yml new file mode 100644 index 0000000..a329a35 --- /dev/null +++ b/ansible/roles/images/tasks/main.yml @@ -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" diff --git a/docs/journal.md b/docs/journal.md index 35eb949..48aeb7b 100644 --- a/docs/journal.md +++ b/docs/journal.md @@ -196,3 +196,41 @@ Kolla-Ansible déploie OpenStack sous forme de conteneurs Docker : chaque servic **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. + +## 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. diff --git a/docs/runbooks/images.md b/docs/runbooks/images.md new file mode 100644 index 0000000..f76b26c --- /dev/null +++ b/docs/runbooks/images.md @@ -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 +``` + +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 "" "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. diff --git a/openstack/images/construire-paillasse-debian.sh b/openstack/images/construire-paillasse-debian.sh new file mode 100644 index 0000000..e27ebb2 --- /dev/null +++ b/openstack/images/construire-paillasse-debian.sh @@ -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é ==" diff --git a/openstack/images/test-console-serie.py b/openstack/images/test-console-serie.py new file mode 100644 index 0000000..a5a9168 --- /dev/null +++ b/openstack/images/test-console-serie.py @@ -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 +""" +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 ===") diff --git a/paillasse-contenus/07-sql/lycee.sql b/paillasse-contenus/07-sql/lycee.sql new file mode 100644 index 0000000..3e29307 --- /dev/null +++ b/paillasse-contenus/07-sql/lycee.sql @@ -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);