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>
2026-10-08 21:52:16 +02:00
|
|
|
// Correcteur Syllabus (étape 11) : exécute dans les VM des élèves les
|
|
|
|
|
// vérifications déclarées dans le lab.yaml, par l'agent invité QEMU.
|
|
|
|
|
//
|
|
|
|
|
// Il écoute sur un socket Unix accessible au seul groupe www-data (le
|
2026-10-12 21:12:43 +02:00
|
|
|
// portail) et n'accepte que deux formes de requête :
|
|
|
|
|
// - {instance, lab, verification} : la commande exécutée est toujours lue
|
|
|
|
|
// dans le lab.yaml, jamais reçue du portail ;
|
|
|
|
|
// - {instance, sante: true} (bloc A, self-healing) : état libvirt de la VM
|
|
|
|
|
// et guest-ping de son agent, sans rien exécuter dans la VM.
|
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>
2026-10-08 21:52:16 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bufio"
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"io"
|
|
|
|
|
"log"
|
|
|
|
|
"net"
|
|
|
|
|
"os"
|
|
|
|
|
"regexp"
|
|
|
|
|
"strconv"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
tailleMaxRequete = 1024
|
|
|
|
|
delaiConnexion = 20 * time.Second
|
|
|
|
|
// README du pack de contenus : 10 s maximum par commande.
|
|
|
|
|
delaiVerification = 10 * time.Second
|
|
|
|
|
maxSimultanees = 8
|
2026-10-12 21:12:43 +02:00
|
|
|
// Sonde de santé : dumpxml + domstate + guest-ping (3 s).
|
|
|
|
|
delaiSonde = 8 * time.Second
|
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>
2026-10-08 21:52:16 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
reInstance = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
|
|
|
|
|
reIdent = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,63}$`)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type requete struct {
|
|
|
|
|
Instance string `json:"instance"`
|
|
|
|
|
Lab string `json:"lab"`
|
|
|
|
|
Verification string `json:"verification"`
|
2026-10-12 21:12:43 +02:00
|
|
|
Sante bool `json:"sante"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// valide : une sonde de santé ne porte ni lab ni vérification ; une
|
|
|
|
|
// vérification porte les deux.
|
|
|
|
|
func (r requete) valide() bool {
|
|
|
|
|
if !reInstance.MatchString(r.Instance) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if r.Sante {
|
|
|
|
|
return r.Lab == "" && r.Verification == ""
|
|
|
|
|
}
|
|
|
|
|
return reIdent.MatchString(r.Lab) && reIdent.MatchString(r.Verification)
|
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>
2026-10-08 21:52:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type reponse struct {
|
|
|
|
|
Reussi bool `json:"reussi"`
|
|
|
|
|
Erreur string `json:"erreur,omitempty"`
|
2026-10-12 21:12:43 +02:00
|
|
|
// Sonde de santé seulement : état libvirt (« running », « shut off »,
|
|
|
|
|
// « paused », « crashed »…).
|
|
|
|
|
Etat string `json:"etat,omitempty"`
|
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>
2026-10-08 21:52:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Codes d'erreur renvoyés au portail (jamais la sortie de la commande).
|
|
|
|
|
const (
|
|
|
|
|
errRequete = "requete_invalide"
|
|
|
|
|
errLab = "lab_inconnu"
|
|
|
|
|
errVerification = "verification_inconnue"
|
|
|
|
|
errMachine = "machine_non_conforme"
|
|
|
|
|
errAgent = "agent_injoignable"
|
|
|
|
|
errDelai = "delai_depasse"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type config struct {
|
|
|
|
|
socket string
|
|
|
|
|
dossierLabs string
|
|
|
|
|
projet string
|
|
|
|
|
uidClient int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
cfg := config{
|
|
|
|
|
socket: envOu("CORRECTEUR_SOCKET", "/run/correcteur/correcteur.sock"),
|
|
|
|
|
dossierLabs: envOu("CORRECTEUR_LABS", "/opt/labs"),
|
|
|
|
|
projet: os.Getenv("CORRECTEUR_PROJET"),
|
|
|
|
|
}
|
|
|
|
|
uid, err := strconv.Atoi(os.Getenv("CORRECTEUR_UID_CLIENT"))
|
|
|
|
|
if err != nil || cfg.projet == "" {
|
|
|
|
|
log.Fatal("CORRECTEUR_PROJET et CORRECTEUR_UID_CLIENT sont obligatoires")
|
|
|
|
|
}
|
|
|
|
|
cfg.uidClient = uid
|
|
|
|
|
|
|
|
|
|
_ = os.Remove(cfg.socket)
|
|
|
|
|
ecoute, err := net.Listen("unix", cfg.socket)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("écoute impossible sur %s : %v", cfg.socket, err)
|
|
|
|
|
}
|
|
|
|
|
// Le groupe du processus est www-data (unité systemd) : 0660 réserve le
|
|
|
|
|
// socket à root et au portail.
|
|
|
|
|
if err := os.Chmod(cfg.socket, 0o660); err != nil {
|
|
|
|
|
log.Fatalf("chmod du socket : %v", err)
|
|
|
|
|
}
|
|
|
|
|
log.Printf("correcteur prêt sur %s (labs : %s)", cfg.socket, cfg.dossierLabs)
|
|
|
|
|
|
|
|
|
|
places := make(chan struct{}, maxSimultanees)
|
|
|
|
|
for {
|
|
|
|
|
conn, err := ecoute.Accept()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("accept : %v", err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
places <- struct{}{}
|
|
|
|
|
go func() {
|
|
|
|
|
defer func() { <-places }()
|
|
|
|
|
traiter(cfg, conn.(*net.UnixConn))
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func traiter(cfg config, conn *net.UnixConn) {
|
|
|
|
|
defer conn.Close()
|
|
|
|
|
_ = conn.SetDeadline(time.Now().Add(delaiConnexion))
|
|
|
|
|
debut := time.Now()
|
|
|
|
|
|
|
|
|
|
// Défense en profondeur : en plus des droits du socket, on vérifie
|
|
|
|
|
// l'identité du processus à l'autre bout (SO_PEERCRED).
|
|
|
|
|
uid, err := uidPair(conn)
|
|
|
|
|
if err != nil || (uid != cfg.uidClient && uid != 0) {
|
|
|
|
|
log.Printf("connexion refusée (uid %d, %v)", uid, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var req requete
|
|
|
|
|
ligne, err := bufio.NewReader(io.LimitReader(conn, tailleMaxRequete)).ReadBytes('\n')
|
|
|
|
|
if err == nil {
|
|
|
|
|
dec := json.NewDecoder(bytes.NewReader(ligne))
|
|
|
|
|
dec.DisallowUnknownFields()
|
|
|
|
|
err = dec.Decode(&req)
|
|
|
|
|
}
|
|
|
|
|
var rep reponse
|
2026-10-12 21:12:43 +02:00
|
|
|
switch {
|
|
|
|
|
case err != nil || !req.valide():
|
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>
2026-10-08 21:52:16 +02:00
|
|
|
rep = reponse{Erreur: errRequete}
|
2026-10-12 21:12:43 +02:00
|
|
|
case req.Sante:
|
|
|
|
|
rep = sonder(cfg, req.Instance)
|
|
|
|
|
default:
|
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>
2026-10-08 21:52:16 +02:00
|
|
|
rep = verifier(cfg, req)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sortie, _ := json.Marshal(rep)
|
|
|
|
|
_, _ = conn.Write(append(sortie, '\n'))
|
2026-10-12 21:12:43 +02:00
|
|
|
if req.Sante {
|
|
|
|
|
// Une sonde par VM et par minute : on ne journalise que les échecs.
|
|
|
|
|
if !rep.Reussi {
|
|
|
|
|
log.Printf("sante instance=%s etat=%q erreur=%q duree=%s", req.Instance, rep.Etat, rep.Erreur, time.Since(debut).Round(time.Millisecond))
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
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>
2026-10-08 21:52:16 +02:00
|
|
|
log.Printf("instance=%s lab=%s verification=%s reussi=%t erreur=%q duree=%s",
|
|
|
|
|
req.Instance, req.Lab, req.Verification, rep.Reussi, rep.Erreur, time.Since(debut).Round(time.Millisecond))
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 21:12:43 +02:00
|
|
|
// sonder : la VM appartient-elle au projet, dans quel état est-elle, et son
|
|
|
|
|
// agent répond-il ? Reussi = agent qui répond.
|
|
|
|
|
func sonder(cfg config, instance string) reponse {
|
|
|
|
|
ctx, annuler := context.WithTimeout(context.Background(), delaiSonde)
|
|
|
|
|
defer annuler()
|
|
|
|
|
|
|
|
|
|
dom, err := lireDomaine(ctx, instance)
|
|
|
|
|
if err != nil || dom.projet() != cfg.projet {
|
|
|
|
|
return reponse{Erreur: errMachine}
|
|
|
|
|
}
|
|
|
|
|
etat, err := etatDomaine(ctx, instance)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return reponse{Erreur: errMachine}
|
|
|
|
|
}
|
|
|
|
|
if etat != "running" {
|
|
|
|
|
return reponse{Etat: etat, Erreur: errAgent}
|
|
|
|
|
}
|
|
|
|
|
if err := pingAgent(ctx, instance); err != nil {
|
|
|
|
|
return reponse{Etat: etat, Erreur: errAgent}
|
|
|
|
|
}
|
|
|
|
|
return reponse{Reussi: true, Etat: etat}
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-10-08 21:52:16 +02:00
|
|
|
func verifier(cfg config, req requete) reponse {
|
|
|
|
|
lab, err := chargerLab(cfg.dossierLabs, req.Lab)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return reponse{Erreur: errLab}
|
|
|
|
|
}
|
|
|
|
|
v, ok := lab.verification(req.Verification)
|
|
|
|
|
if !ok {
|
|
|
|
|
return reponse{Erreur: errVerification}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx, annuler := context.WithTimeout(context.Background(), delaiVerification+5*time.Second)
|
|
|
|
|
defer annuler()
|
|
|
|
|
|
|
|
|
|
dom, err := lireDomaine(ctx, req.Instance)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("dumpxml %s : %v", req.Instance, err)
|
|
|
|
|
return reponse{Erreur: errMachine}
|
|
|
|
|
}
|
|
|
|
|
if dom.projet() != cfg.projet || lab.machineDuNom(dom.nom()) != v.Machine {
|
|
|
|
|
log.Printf("instance %s refusée : projet %q, nom %q, machine attendue %q", req.Instance, dom.projet(), dom.nom(), v.Machine)
|
|
|
|
|
return reponse{Erreur: errMachine}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctxExec, annulerExec := context.WithTimeout(ctx, delaiVerification)
|
|
|
|
|
defer annulerExec()
|
|
|
|
|
code, sortieCmd, err := executer(ctxExec, req.Instance, v.Commande)
|
|
|
|
|
switch {
|
|
|
|
|
case errors.Is(err, errDelaiDepasse):
|
|
|
|
|
return reponse{Erreur: errDelai}
|
|
|
|
|
case err != nil:
|
|
|
|
|
log.Printf("guest-exec %s : %v", req.Instance, err)
|
|
|
|
|
return reponse{Erreur: errAgent}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return reponse{Reussi: v.Attendu.satisfait(code, sortieCmd)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func envOu(cle, defaut string) string {
|
|
|
|
|
if v := os.Getenv(cle); v != "" {
|
|
|
|
|
return v
|
|
|
|
|
}
|
|
|
|
|
return defaut
|
|
|
|
|
}
|