package main import ( "context" "encoding/base64" "encoding/json" "encoding/xml" "errors" "fmt" "os/exec" "strings" "time" ) var errDelaiDepasse = errors.New("délai dépassé") // virsh lancé dans le conteneur nova_libvirt de Kolla. Les arguments sont // passés tels quels (pas de shell) : rien n'est interprété sur l'hôte. func virsh(ctx context.Context, args ...string) ([]byte, error) { cmd := exec.CommandContext(ctx, "docker", append([]string{"exec", "nova_libvirt", "virsh"}, args...)...) sortie, err := cmd.Output() if err != nil { var ee *exec.ExitError if errors.As(err, &ee) { return nil, fmt.Errorf("%w : %s", err, ee.Stderr) } return nil, err } return sortie, nil } // Métadonnées que Nova écrit dans la définition libvirt de chaque instance // (espace de noms http://openstack.org/xmlns/libvirt/nova/…). type domaine struct { Metadata struct { Instance struct { Nom string `xml:"name"` Owner struct { Projet struct { UUID string `xml:"uuid,attr"` } `xml:"project"` } `xml:"owner"` } `xml:"instance"` } `xml:"metadata"` } func (d domaine) nom() string { return d.Metadata.Instance.Nom } func (d domaine) projet() string { return d.Metadata.Instance.Owner.Projet.UUID } func lireDomaine(ctx context.Context, instance string) (domaine, error) { var d domaine brut, err := virsh(ctx, "dumpxml", instance) if err != nil { return d, err } err = xml.Unmarshal(brut, &d) 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 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) { lancement, _ := json.Marshal(map[string]any{ "execute": "guest-exec", "arguments": map[string]any{ "path": "/bin/sh", "arg": []string{"-c", commande}, "capture-output": true, }, }) brut, err := virsh(ctx, "qemu-agent-command", instance, string(lancement), "--timeout", "5") if err != nil { return 0, "", err } var pid struct { Return struct { PID int `json:"pid"` } `json:"return"` } if err := json.Unmarshal(brut, &pid); err != nil { return 0, "", err } statut, _ := json.Marshal(map[string]any{"execute": "guest-exec-status", "arguments": map[string]int{"pid": pid.Return.PID}}) for { select { case <-ctx.Done(): return 0, "", errDelaiDepasse case <-time.After(200 * time.Millisecond): } brut, err := virsh(ctx, "qemu-agent-command", instance, string(statut), "--timeout", "5") if err != nil { if ctx.Err() != nil { return 0, "", errDelaiDepasse } return 0, "", err } var r struct { Return struct { Exited bool `json:"exited"` ExitCode int `json:"exitcode"` OutData string `json:"out-data"` } `json:"return"` } if err := json.Unmarshal(brut, &r); err != nil { return 0, "", err } if r.Return.Exited { sortie, _ := base64.StdEncoding.DecodeString(r.Return.OutData) return r.Return.ExitCode, string(sortie), nil } } }