130 lines
3.5 KiB
Go
130 lines
3.5 KiB
Go
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 <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) {
|
|
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
|
|
}
|
|
}
|
|
}
|