Compare commits

..

7 commits

Author SHA1 Message Date
a3b85f5926
Merge branch 'feature-32'
All checks were successful
Pre Release Workflow / set-release-target (push) Successful in 1s
Pre Release Workflow / build (metadata, amd64, linux) (push) Successful in 1m40s
Pre Release Workflow / build (agent, amd64, linux) (push) Successful in 2m1s
Pre Release Workflow / upload-scripts (run-dnsmasq-in-netns.sh) (push) Successful in 6s
Pre Release Workflow / prerelease (push) Successful in 10s
2026-06-14 23:05:16 +02:00
4ab880a32d
f-32: vm: fix incomplete stop vm
All checks were successful
Pre Release Workflow / set-release-target (push) Successful in 1s
Pre Release Workflow / build (agent, amd64, linux) (push) Successful in 1m36s
Pre Release Workflow / build (metadata, amd64, linux) (push) Successful in 2m19s
Pre Release Workflow / upload-scripts (run-dnsmasq-in-netns.sh) (push) Successful in 6s
Pre Release Workflow / prerelease (push) Successful in 11s
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-05-24 19:02:31 +02:00
c3f26836cd
f-32: fix: update boot order
All checks were successful
Pre Release Workflow / set-release-target (push) Successful in 1s
Pre Release Workflow / upload-scripts (run-dnsmasq-in-netns.sh) (push) Successful in 6s
Pre Release Workflow / build (agent, amd64, linux) (push) Successful in 1m34s
Pre Release Workflow / build (metadata, amd64, linux) (push) Successful in 1m31s
Pre Release Workflow / prerelease (push) Successful in 10s
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-05-24 16:47:49 +02:00
9111045417
f-32: test: add test for vms
All checks were successful
Pre Release Workflow / set-release-target (push) Successful in 1s
Pre Release Workflow / build (agent, amd64, linux) (push) Successful in 3m33s
Pre Release Workflow / build (metadata, amd64, linux) (push) Successful in 1m31s
Pre Release Workflow / upload-scripts (run-dnsmasq-in-netns.sh) (push) Successful in 6s
Pre Release Workflow / prerelease (push) Successful in 10s
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-05-24 16:18:53 +02:00
a0637d827a
f-32: syntax: fix duplicated code
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-05-24 16:13:11 +02:00
89899005d9
f-32: disk: add disk in qemu start
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-05-24 15:56:45 +02:00
85c8c4e590
f-32: disk: add boot multidisk handle
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-05-24 15:44:48 +02:00
11 changed files with 436 additions and 69 deletions

View file

@ -87,8 +87,11 @@ func vmFromDB(name string, entries map[string]string) (VM, error) {
vm.Interfaces = []VMInterface{{Subnet: subnet, IP: ip, Primary: true}} vm.Interfaces = []VMInterface{{Subnet: subnet, IP: ip, Primary: true}}
} }
if path := entries[prefix+"volume_path"]; path != "" { diskPrefix := prefix + "disk/"
vm.Storage = []VMStorage{{Path: path}} for key, path := range entries {
if dev := strings.TrimPrefix(key, diskPrefix); dev != key {
vm.Storage = append(vm.Storage, VMStorage{Path: path, Dev: dev})
}
} }
return vm, nil return vm, nil

View file

@ -0,0 +1,158 @@
package agentapi
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"sort"
"testing"
"git.g3e.fr/syonad/two/pkg/db/kv"
)
// --- vmFromDB ---
func TestVmFromDB_SingleDisk(t *testing.T) {
entries := map[string]string{
"vm/vm-1/state": "started",
"vm/vm-1/subnet": "sn-1",
"vm/vm-1/ip": "10.0.0.5",
"vm/vm-1/metadata_port": "1234",
"vm/vm-1/memory": "512",
"vm/vm-1/cpus": "1",
"vm/vm-1/disk/sda": "/data/root.qcow2",
}
vm, err := vmFromDB("vm-1", entries)
if err != nil {
t.Fatalf("vmFromDB a échoué : %v", err)
}
if len(vm.Storage) != 1 {
t.Fatalf("attendu 1 disque, obtenu %d", len(vm.Storage))
}
if vm.Storage[0].Dev != "sda" || vm.Storage[0].Path != "/data/root.qcow2" {
t.Errorf("disque inattendu : %+v", vm.Storage[0])
}
}
func TestVmFromDB_MultiDisk(t *testing.T) {
entries := map[string]string{
"vm/vm-2/state": "started",
"vm/vm-2/subnet": "sn-1",
"vm/vm-2/ip": "10.0.0.6",
"vm/vm-2/metadata_port": "1235",
"vm/vm-2/memory": "1024",
"vm/vm-2/cpus": "2",
"vm/vm-2/disk/sda": "/data/root.qcow2",
"vm/vm-2/disk/sdb": "/data/data.qcow2",
}
vm, err := vmFromDB("vm-2", entries)
if err != nil {
t.Fatalf("vmFromDB a échoué : %v", err)
}
if len(vm.Storage) != 2 {
t.Fatalf("attendu 2 disques, obtenu %d", len(vm.Storage))
}
sort.Slice(vm.Storage, func(i, j int) bool { return vm.Storage[i].Dev < vm.Storage[j].Dev })
if vm.Storage[0].Dev != "sda" || vm.Storage[1].Dev != "sdb" {
t.Errorf("devs attendus [sda sdb], obtenus [%s %s]", vm.Storage[0].Dev, vm.Storage[1].Dev)
}
}
func TestVmFromDB_SlotGap(t *testing.T) {
// sdb absent — sda et sdc seulement
entries := map[string]string{
"vm/vm-3/state": "started",
"vm/vm-3/subnet": "sn-1",
"vm/vm-3/ip": "10.0.0.7",
"vm/vm-3/metadata_port": "1236",
"vm/vm-3/memory": "512",
"vm/vm-3/cpus": "1",
"vm/vm-3/disk/sda": "/data/root.qcow2",
"vm/vm-3/disk/sdc": "/data/extra.qcow2",
}
vm, err := vmFromDB("vm-3", entries)
if err != nil {
t.Fatalf("vmFromDB a échoué : %v", err)
}
if len(vm.Storage) != 2 {
t.Fatalf("attendu 2 disques, obtenu %d", len(vm.Storage))
}
devs := map[string]bool{}
for _, s := range vm.Storage {
devs[s.Dev] = true
}
if !devs["sda"] || !devs["sdc"] {
t.Errorf("attendu sda et sdc, obtenus %v", devs)
}
if devs["sdb"] {
t.Error("sdb ne devrait pas apparaître")
}
}
// --- POST /vms ---
func TestStartVM_MultiDisk(t *testing.T) {
s, db := newTestServer(t)
kv.AddInDB(db, "subnet/sn-1/state", "created")
kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-1")
body, _ := json.Marshal(VMCreateRequest{
Name: "vm-10",
Interfaces: []VMInterface{
{Subnet: "sn-1", IP: "10.0.0.10", Primary: true},
},
Storage: []VMStorage{
{Path: "/data/root.qcow2", Dev: "sda"},
{Path: "/data/data.qcow2", Dev: "sdb"},
},
Memory: 1024,
CPUs: 2,
})
w := httptest.NewRecorder()
s.VmsHandler(w, httptest.NewRequest(http.MethodPost, "/vms", bytes.NewReader(body)))
if w.Code != http.StatusAccepted {
t.Fatalf("attendu 202, obtenu %d : %s", w.Code, w.Body.String())
}
for _, dev := range []string{"sda", "sdb"} {
if _, err := kv.GetFromDB(db, "vm/vm-10/disk/"+dev); err != nil {
t.Errorf("disk/%s absent en DB après création", dev)
}
}
if _, err := kv.GetFromDB(db, "vm/vm-10/volume_path"); err == nil {
t.Error("volume_path ne devrait plus exister en DB")
}
}
func TestStartVM_StorageReturnedInResponse(t *testing.T) {
s, db := newTestServer(t)
kv.AddInDB(db, "subnet/sn-1/state", "created")
kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-1")
body, _ := json.Marshal(VMCreateRequest{
Name: "vm-11",
Interfaces: []VMInterface{
{Subnet: "sn-1", IP: "10.0.0.11", Primary: true},
},
Storage: []VMStorage{
{Path: "/data/root.qcow2", Dev: "sda"},
},
})
w := httptest.NewRecorder()
s.VmsHandler(w, httptest.NewRequest(http.MethodPost, "/vms", bytes.NewReader(body)))
if w.Code != http.StatusAccepted {
t.Fatalf("attendu 202, obtenu %d", w.Code)
}
var vm VM
json.NewDecoder(w.Body).Decode(&vm)
if len(vm.Storage) != 1 {
t.Fatalf("attendu 1 disque dans la réponse, obtenu %d", len(vm.Storage))
}
if vm.Storage[0].Dev != "sda" || vm.Storage[0].Path != "/data/root.qcow2" {
t.Errorf("disque inattendu dans la réponse : %+v", vm.Storage[0])
}
}

View file

@ -77,16 +77,21 @@ func (s *Server) startVM(w http.ResponseWriter, r *http.Request) {
return return
} }
disks := make([]dispatcher.VMDisk, len(req.Storage))
for i, s := range req.Storage {
disks[i] = dispatcher.VMDisk{Path: s.Path, Dev: s.Dev}
}
cmd := dispatcher.StartVMCommand{ cmd := dispatcher.StartVMCommand{
Name: req.Name, Name: req.Name,
Subnet: primary.Subnet, Subnet: primary.Subnet,
IP: primary.IP, IP: primary.IP,
VolumePath: req.Storage[0].Path, Disks: disks,
Memory: req.Memory, Memory: req.Memory,
CPUs: req.CPUs, CPUs: req.CPUs,
UEFI: req.UEFI, UEFI: req.UEFI,
Password: req.Password, Password: req.Password,
SSHKey: req.SSHKey, SSHKey: req.SSHKey,
} }
if err := s.dispatcher.Prepare(cmd); err != nil { if err := s.dispatcher.Prepare(cmd); err != nil {

View file

@ -13,16 +13,21 @@ import (
"github.com/dgraph-io/badger/v4" "github.com/dgraph-io/badger/v4"
) )
type VMDisk struct {
Path string
Dev string
}
type StartVMCommand struct { type StartVMCommand struct {
Name string Name string
Subnet string Subnet string
IP string IP string
VolumePath string Disks []VMDisk
Memory int Memory int
CPUs int CPUs int
UEFI bool UEFI bool
Password string Password string
SSHKey string SSHKey string
} }
func (c StartVMCommand) Prepare(db *badger.DB, _ *configuration.Config) error { func (c StartVMCommand) Prepare(db *badger.DB, _ *configuration.Config) error {
@ -44,7 +49,9 @@ func (c StartVMCommand) Prepare(db *badger.DB, _ *configuration.Config) error {
kv.AddInDB(db, "vm/"+c.Name+"/subnet", c.Subnet) kv.AddInDB(db, "vm/"+c.Name+"/subnet", c.Subnet)
kv.AddInDB(db, "vm/"+c.Name+"/ip", c.IP) kv.AddInDB(db, "vm/"+c.Name+"/ip", c.IP)
kv.AddInDB(db, "vm/"+c.Name+"/metadata_port", strconv.Itoa(port)) kv.AddInDB(db, "vm/"+c.Name+"/metadata_port", strconv.Itoa(port))
kv.AddInDB(db, "vm/"+c.Name+"/volume_path", c.VolumePath) for _, d := range c.Disks {
kv.AddInDB(db, "vm/"+c.Name+"/disk/"+d.Dev, d.Path)
}
kv.AddInDB(db, "vm/"+c.Name+"/memory", strconv.Itoa(c.Memory)) kv.AddInDB(db, "vm/"+c.Name+"/memory", strconv.Itoa(c.Memory))
kv.AddInDB(db, "vm/"+c.Name+"/cpus", strconv.Itoa(c.CPUs)) kv.AddInDB(db, "vm/"+c.Name+"/cpus", strconv.Itoa(c.CPUs))
if c.UEFI { if c.UEFI {

View file

@ -0,0 +1,130 @@
package dispatcher
import (
"testing"
"git.g3e.fr/syonad/two/pkg/db/kv"
)
// --- StartVMCommand.Prepare : écriture des disques en DB ---
func TestStartVMCommand_Prepare_SingleDisk(t *testing.T) {
_, db := newTestDispatcher(t)
kv.AddInDB(db, "subnet/sn-1/state", "created")
kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-1")
cmd := StartVMCommand{
Name: "vm-1",
Subnet: "sn-1",
IP: "10.0.0.5",
Disks: []VMDisk{{Path: "/data/root.qcow2", Dev: "sda"}},
}
if err := cmd.Prepare(db, nil); err != nil {
t.Fatalf("Prepare a échoué : %v", err)
}
path, err := kv.GetFromDB(db, "vm/vm-1/disk/sda")
if err != nil {
t.Fatalf("clé disk/sda absente en DB : %v", err)
}
if path != "/data/root.qcow2" {
t.Errorf("path attendu /data/root.qcow2, obtenu %q", path)
}
}
func TestStartVMCommand_Prepare_MultiDisk(t *testing.T) {
_, db := newTestDispatcher(t)
kv.AddInDB(db, "subnet/sn-1/state", "created")
kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-1")
cmd := StartVMCommand{
Name: "vm-2",
Subnet: "sn-1",
IP: "10.0.0.6",
Disks: []VMDisk{
{Path: "/data/root.qcow2", Dev: "sda"},
{Path: "/data/data.qcow2", Dev: "sdb"},
},
}
if err := cmd.Prepare(db, nil); err != nil {
t.Fatalf("Prepare a échoué : %v", err)
}
for dev, want := range map[string]string{
"sda": "/data/root.qcow2",
"sdb": "/data/data.qcow2",
} {
got, err := kv.GetFromDB(db, "vm/vm-2/disk/"+dev)
if err != nil {
t.Fatalf("clé disk/%s absente en DB : %v", dev, err)
}
if got != want {
t.Errorf("disk/%s : attendu %q, obtenu %q", dev, want, got)
}
}
}
func TestStartVMCommand_Prepare_SlotGap(t *testing.T) {
_, db := newTestDispatcher(t)
kv.AddInDB(db, "subnet/sn-1/state", "created")
kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-1")
// sdb absent au boot — slot réservé pour hotplug
cmd := StartVMCommand{
Name: "vm-3",
Subnet: "sn-1",
IP: "10.0.0.7",
Disks: []VMDisk{
{Path: "/data/root.qcow2", Dev: "sda"},
{Path: "/data/extra.qcow2", Dev: "sdc"},
},
}
if err := cmd.Prepare(db, nil); err != nil {
t.Fatalf("Prepare a échoué : %v", err)
}
if _, err := kv.GetFromDB(db, "vm/vm-3/disk/sda"); err != nil {
t.Fatalf("disk/sda absent : %v", err)
}
if _, err := kv.GetFromDB(db, "vm/vm-3/disk/sdc"); err != nil {
t.Fatalf("disk/sdc absent : %v", err)
}
if _, err := kv.GetFromDB(db, "vm/vm-3/disk/sdb"); err == nil {
t.Error("disk/sdb ne devrait pas exister en DB")
}
}
func TestStartVMCommand_Prepare_NoVolumePath(t *testing.T) {
_, db := newTestDispatcher(t)
kv.AddInDB(db, "subnet/sn-1/state", "created")
kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-1")
cmd := StartVMCommand{
Name: "vm-4",
Subnet: "sn-1",
IP: "10.0.0.8",
Disks: []VMDisk{{Path: "/data/root.qcow2", Dev: "sda"}},
}
if err := cmd.Prepare(db, nil); err != nil {
t.Fatalf("Prepare a échoué : %v", err)
}
if _, err := kv.GetFromDB(db, "vm/vm-4/volume_path"); err == nil {
t.Error("volume_path ne devrait plus être écrit en DB")
}
}
func TestStartVMCommand_Prepare_Duplicate(t *testing.T) {
_, db := newTestDispatcher(t)
kv.AddInDB(db, "vm/vm-exist/state", "started")
cmd := StartVMCommand{
Name: "vm-exist",
Subnet: "sn-1",
IP: "10.0.0.9",
Disks: []VMDisk{{Path: "/data/root.qcow2", Dev: "sda"}},
}
if err := cmd.Prepare(db, nil); err == nil {
t.Error("Prepare devrait échouer si la VM existe déjà")
}
}

20
internal/qemu/config.go Normal file
View file

@ -0,0 +1,20 @@
package qemu
type DiskConfig struct {
Path string
Dev string
}
type Config struct {
Name string
TapID int
Mac string
Disks []DiskConfig
Memory int
CPUs int
UEFICodePath string
UEFIVarsPath string
SerialDir string
MonitorDir string
QMPDir string
}

View file

@ -7,22 +7,10 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"sort"
"strings"
) )
type Config struct {
Name string
TapID int
Mac string
VolumePath string
Memory int
CPUs int
UEFICodePath string
UEFIVarsPath string
SerialDir string
MonitorDir string
QMPDir string
}
func Start(cfg Config) error { func Start(cfg Config) error {
memory := cfg.Memory memory := cfg.Memory
if memory == 0 { if memory == 0 {
@ -64,8 +52,47 @@ func Start(cfg Config) error {
) )
} }
hasScsi := false
for _, d := range cfg.Disks {
if strings.HasPrefix(d.Dev, "sd") {
hasScsi = true
break
}
}
if hasScsi {
args = append(args, "-device", "virtio-scsi-pci,id=scsi0")
}
sorted := make([]DiskConfig, len(cfg.Disks))
copy(sorted, cfg.Disks)
// vd* avant sd* : les disques virtio-blk bootent en premier.
// À lettre égale de type, ordre alphabétique.
sort.Slice(sorted, func(i, j int) bool {
iVirtio := strings.HasPrefix(sorted[i].Dev, "vd")
jVirtio := strings.HasPrefix(sorted[j].Dev, "vd")
if iVirtio != jVirtio {
return iVirtio
}
return sorted[i].Dev < sorted[j].Dev
})
for idx, d := range sorted {
bootindex := idx + 1
if strings.HasPrefix(d.Dev, "sd") {
scsiID := int(d.Dev[2] - 'a')
args = append(args,
"-drive", fmt.Sprintf("file=%s,if=none,id=%s", d.Path, d.Dev),
"-device", fmt.Sprintf("scsi-hd,drive=%s,bus=scsi0.0,scsi-id=%d,bootindex=%d", d.Dev, scsiID, bootindex),
)
} else {
args = append(args,
"-drive", fmt.Sprintf("file=%s,if=none,id=%s", d.Path, d.Dev),
"-device", fmt.Sprintf("virtio-blk-pci,drive=%s,bootindex=%d", d.Dev, bootindex),
)
}
}
args = append(args, args = append(args,
"-drive", fmt.Sprintf("file=%s,if=virtio", cfg.VolumePath),
"-netdev", fmt.Sprintf("tap,id=net0,ifname=tap%d,script=no,downscript=no", cfg.TapID), "-netdev", fmt.Sprintf("tap,id=net0,ifname=tap%d,script=no,downscript=no", cfg.TapID),
"-device", fmt.Sprintf("virtio-net-pci,netdev=net0,mac=%s", cfg.Mac), "-device", fmt.Sprintf("virtio-net-pci,netdev=net0,mac=%s", cfg.Mac),
"-daemonize", "-daemonize",

View file

@ -2,17 +2,9 @@
package qemu package qemu
import "errors" import (
"errors"
type Config struct { )
Name, Mac, VolumePath string
TapID, Memory, CPUs int
UEFICodePath string
UEFIVarsPath string
SerialDir string
MonitorDir string
QMPDir string
}
func Start(_ Config) error { func Start(_ Config) error {
return errors.New("vm: not supported on this platform") return errors.New("vm: not supported on this platform")

View file

@ -52,11 +52,16 @@ func StartVM(db *badger.DB, name string, cfg *configuration.Config) error {
return fmt.Errorf("start metadata: %w", err) return fmt.Errorf("start metadata: %w", err)
} }
qDisks := make([]qemu.DiskConfig, len(d.disks))
for i, disk := range d.disks {
qDisks[i] = qemu.DiskConfig{Path: disk.path, Dev: disk.dev}
}
qcfg := qemu.Config{ qcfg := qemu.Config{
Name: name, Name: name,
TapID: d.tapID, TapID: d.tapID,
Mac: d.mac, Mac: d.mac,
VolumePath: d.volumePath, Disks: qDisks,
Memory: d.memory, Memory: d.memory,
CPUs: d.cpus, CPUs: d.cpus,
SerialDir: cfg.QEMU.SerialDir, SerialDir: cfg.QEMU.SerialDir,

View file

@ -11,6 +11,11 @@ import (
"github.com/dgraph-io/badger/v4" "github.com/dgraph-io/badger/v4"
) )
type diskEntry struct {
path string
dev string
}
type vmData struct { type vmData struct {
subnetName string subnetName string
vpcName string vpcName string
@ -20,7 +25,7 @@ type vmData struct {
ip string ip string
metadataPort string metadataPort string
mac string mac string
volumePath string disks []diskEntry
memory int memory int
cpus int cpus int
uefi bool uefi bool
@ -82,11 +87,18 @@ func loadVM(db *badger.DB, name string) (vmData, error) {
} }
d.mac = mac d.mac = mac
volumePath, err := kv.GetFromDB(db, "vm/"+name+"/volume_path") diskEntries, err := kv.ListByPrefix(db, "vm/"+name+"/disk/")
if err != nil { if err != nil {
return d, fmt.Errorf("get volume_path: %w", err) return d, fmt.Errorf("list disks: %w", err)
}
if len(diskEntries) == 0 {
return d, fmt.Errorf("no disks found for vm %q", name)
}
diskPrefix := "vm/" + name + "/disk/"
for key, path := range diskEntries {
dev := strings.TrimPrefix(key, diskPrefix)
d.disks = append(d.disks, diskEntry{path: path, dev: dev})
} }
d.volumePath = volumePath
memoryStr, err := kv.GetFromDB(db, "vm/"+name+"/memory") memoryStr, err := kv.GetFromDB(db, "vm/"+name+"/memory")
if err != nil { if err != nil {

View file

@ -33,25 +33,18 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error {
socketPath := filepath.Join(cfg.QEMU.QMPDir, name+".sock") socketPath := filepath.Join(cfg.QEMU.QMPDir, name+".sock")
if _, err := qmp.Send(socketPath, []string{`{"execute":"system_powerdown"}`}); err != nil { if _, err := os.Stat(socketPath); err == nil {
return fmt.Errorf("qmp system_powerdown: %w", err) // socket présent : tenter l'arrêt gracieux
} if _, err := qmp.Send(socketPath, []string{`{"execute":"system_powerdown"}`}); err == nil {
waitQMPDead(socketPath,
// attendre l'arrêt effectif de la VM ; forcer via quit après timeout time.Duration(cfg.Dispatcher.TimeoutSeconds)*time.Second,
timeout := time.After(time.Duration(cfg.Dispatcher.TimeoutSeconds) * time.Second) time.Duration(cfg.Dispatcher.PollSeconds)*time.Second,
poll := time.Duration(cfg.Dispatcher.PollSeconds) * time.Second )
stopped := false
for !stopped {
select {
case <-timeout:
qmp.Send(socketPath, []string{`{"execute":"quit"}`})
stopped = true
case <-time.After(poll):
if _, err := qmp.Send(socketPath, nil); err != nil {
stopped = true
}
} }
// connexion QMP échouée : QEMU déjà mort
} }
// socket absent ou QEMU déjà arrêté : cleanup direct
if err := netns.Call(d.vpcName, func() error { if err := netns.Call(d.vpcName, func() error {
return iptables.DeleteMetadataRedirect(d.ip, d.interfaceIP, d.metadataPort) return iptables.DeleteMetadataRedirect(d.ip, d.interfaceIP, d.metadataPort)
@ -74,3 +67,18 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error {
return kv.AddInDB(db, "vm/"+name+"/state", "stopped") return kv.AddInDB(db, "vm/"+name+"/state", "stopped")
} }
func waitQMPDead(socketPath string, timeout, poll time.Duration) {
timer := time.After(timeout)
for {
select {
case <-timer:
qmp.Send(socketPath, []string{`{"execute":"quit"}`})
return
case <-time.After(poll):
if _, err := qmp.Send(socketPath, nil); err != nil {
return
}
}
}
}