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

@ -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 {