ajoute le self-healing des VM (sonde de santé, relance automatique, stock écarté) — bloc A, semaine 6

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-12 21:12:43 +02:00
parent b054888f5a
commit f2649d0f8b
21 changed files with 938 additions and 28 deletions

View file

@ -53,3 +53,24 @@ func TestValidationIdentifiants(t *testing.T) {
t.Fatal("validation d'uuid incorrecte")
}
}
func TestRequeteValide(t *testing.T) {
uuid := "8c8d5447-80fe-40da-ae65-812e077941f8"
cas := []struct {
r requete
ok bool
}{
{requete{Instance: uuid, Lab: "08-deux-lan-routeur", Verification: "forwarding"}, true},
{requete{Instance: uuid, Sante: true}, true},
// Une sonde ne peut pas porter de vérification (rien ne s'exécute).
{requete{Instance: uuid, Sante: true, Lab: "08-deux-lan-routeur", Verification: "forwarding"}, false},
{requete{Instance: uuid, Sante: true, Verification: "forwarding"}, false},
{requete{Instance: "pas-un-uuid", Sante: true}, false},
{requete{Instance: uuid}, false},
}
for _, c := range cas {
if got := c.r.valide(); got != c.ok {
t.Errorf("valide(%+v) = %t, attendu %t", c.r, got, c.ok)
}
}
}

View file

@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"os/exec"
"strings"
"time"
)
@ -56,6 +57,22 @@ func lireDomaine(ctx context.Context, instance string) (domaine, error) {
return d, err
}
// État libvirt du domaine : « running », « paused », « shut off », « crashed »…
func etatDomaine(ctx context.Context, instance string) (string, error) {
brut, err := virsh(ctx, "domstate", instance)
if err != nil {
return "", err
}
return strings.TrimSpace(string(brut)), nil
}
// guest-ping : l'agent répond-il ? Un noyau planté ou une VM gelée ne
// répondent plus.
func pingAgent(ctx context.Context, instance string) error {
_, err := virsh(ctx, "qemu-agent-command", instance, `{"execute":"guest-ping"}`, "--timeout", "3")
return err
}
// Lance /bin/sh -c <commande> dans la VM (guest-exec), puis interroge
// guest-exec-status jusqu'à la fin du processus.
func executer(ctx context.Context, instance, commande string) (int, string, error) {

View file

@ -2,9 +2,11 @@
// 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
// portail) et n'accepte que trois identifiants : instance, lab,
// vérification. La commande exécutée est toujours lue dans le lab.yaml,
// jamais reçue du portail.
// 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.
package main
import (
@ -28,6 +30,8 @@ const (
// README du pack de contenus : 10 s maximum par commande.
delaiVerification = 10 * time.Second
maxSimultanees = 8
// Sonde de santé : dumpxml + domstate + guest-ping (3 s).
delaiSonde = 8 * time.Second
)
var (
@ -39,11 +43,27 @@ type requete struct {
Instance string `json:"instance"`
Lab string `json:"lab"`
Verification string `json:"verification"`
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)
}
type reponse struct {
Reussi bool `json:"reussi"`
Erreur string `json:"erreur,omitempty"`
// Sonde de santé seulement : état libvirt (« running », « shut off »,
// « paused », « crashed »…).
Etat string `json:"etat,omitempty"`
}
// Codes d'erreur renvoyés au portail (jamais la sortie de la commande).
@ -123,18 +143,51 @@ func traiter(cfg config, conn *net.UnixConn) {
err = dec.Decode(&req)
}
var rep reponse
if err != nil || !reInstance.MatchString(req.Instance) || !reIdent.MatchString(req.Lab) || !reIdent.MatchString(req.Verification) {
switch {
case err != nil || !req.valide():
rep = reponse{Erreur: errRequete}
} else {
case req.Sante:
rep = sonder(cfg, req.Instance)
default:
rep = verifier(cfg, req)
}
sortie, _ := json.Marshal(rep)
_, _ = conn.Write(append(sortie, '\n'))
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
}
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))
}
// 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}
}
func verifier(cfg config, req requete) reponse {
lab, err := chargerLab(cfg.dossierLabs, req.Lab)
if err != nil {