From 85c8c4e590625148faf75859072fe9fae3e1f302 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 24 May 2026 15:44:48 +0200 Subject: [PATCH 1/6] f-32: disk: add boot multidisk handle Signed-off-by: GnomeZworc --- internal/api/agent/vms.go | 23 ++++++++++++-------- internal/dispatcher/agent/vm_commands.go | 27 +++++++++++++++--------- internal/vm/create.go | 2 +- internal/vm/data.go | 20 ++++++++++++++---- 4 files changed, 48 insertions(+), 24 deletions(-) diff --git a/internal/api/agent/vms.go b/internal/api/agent/vms.go index 4dc6136..2ea5748 100644 --- a/internal/api/agent/vms.go +++ b/internal/api/agent/vms.go @@ -77,16 +77,21 @@ func (s *Server) startVM(w http.ResponseWriter, r *http.Request) { 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{ - Name: req.Name, - Subnet: primary.Subnet, - IP: primary.IP, - VolumePath: req.Storage[0].Path, - Memory: req.Memory, - CPUs: req.CPUs, - UEFI: req.UEFI, - Password: req.Password, - SSHKey: req.SSHKey, + Name: req.Name, + Subnet: primary.Subnet, + IP: primary.IP, + Disks: disks, + Memory: req.Memory, + CPUs: req.CPUs, + UEFI: req.UEFI, + Password: req.Password, + SSHKey: req.SSHKey, } if err := s.dispatcher.Prepare(cmd); err != nil { diff --git a/internal/dispatcher/agent/vm_commands.go b/internal/dispatcher/agent/vm_commands.go index 1c4bbce..bdfeaec 100644 --- a/internal/dispatcher/agent/vm_commands.go +++ b/internal/dispatcher/agent/vm_commands.go @@ -13,16 +13,21 @@ import ( "github.com/dgraph-io/badger/v4" ) +type VMDisk struct { + Path string + Dev string +} + type StartVMCommand struct { - Name string - Subnet string - IP string - VolumePath string - Memory int - CPUs int - UEFI bool - Password string - SSHKey string + Name string + Subnet string + IP string + Disks []VMDisk + Memory int + CPUs int + UEFI bool + Password string + SSHKey string } 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+"/ip", c.IP) 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+"/cpus", strconv.Itoa(c.CPUs)) if c.UEFI { diff --git a/internal/vm/create.go b/internal/vm/create.go index 529c73c..267dab5 100644 --- a/internal/vm/create.go +++ b/internal/vm/create.go @@ -56,7 +56,7 @@ func StartVM(db *badger.DB, name string, cfg *configuration.Config) error { Name: name, TapID: d.tapID, Mac: d.mac, - VolumePath: d.volumePath, + VolumePath: d.disks[0].path, Memory: d.memory, CPUs: d.cpus, SerialDir: cfg.QEMU.SerialDir, diff --git a/internal/vm/data.go b/internal/vm/data.go index 1e2644e..3435fb1 100644 --- a/internal/vm/data.go +++ b/internal/vm/data.go @@ -11,6 +11,11 @@ import ( "github.com/dgraph-io/badger/v4" ) +type diskEntry struct { + path string + dev string +} + type vmData struct { subnetName string vpcName string @@ -20,7 +25,7 @@ type vmData struct { ip string metadataPort string mac string - volumePath string + disks []diskEntry memory int cpus int uefi bool @@ -82,11 +87,18 @@ func loadVM(db *badger.DB, name string) (vmData, error) { } d.mac = mac - volumePath, err := kv.GetFromDB(db, "vm/"+name+"/volume_path") + diskEntries, err := kv.ListByPrefix(db, "vm/"+name+"/disk/") 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") if err != nil { -- 2.49.1 From 89899005d9eb2b9f2e84fe68f5230990090ad956 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 24 May 2026 15:56:45 +0200 Subject: [PATCH 2/6] f-32: disk: add disk in qemu start Signed-off-by: GnomeZworc --- internal/api/agent/vm.go | 7 +++-- internal/qemu/start_linux.go | 55 +++++++++++++++++++++++++++++------- internal/qemu/start_other.go | 23 ++++++++++----- internal/vm/create.go | 7 ++++- 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/internal/api/agent/vm.go b/internal/api/agent/vm.go index f932766..d9f6bf5 100644 --- a/internal/api/agent/vm.go +++ b/internal/api/agent/vm.go @@ -87,8 +87,11 @@ func vmFromDB(name string, entries map[string]string) (VM, error) { vm.Interfaces = []VMInterface{{Subnet: subnet, IP: ip, Primary: true}} } - if path := entries[prefix+"volume_path"]; path != "" { - vm.Storage = []VMStorage{{Path: path}} + diskPrefix := prefix + "disk/" + 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 diff --git a/internal/qemu/start_linux.go b/internal/qemu/start_linux.go index 3f40e73..2043b64 100644 --- a/internal/qemu/start_linux.go +++ b/internal/qemu/start_linux.go @@ -7,20 +7,27 @@ import ( "os" "os/exec" "path/filepath" + "sort" + "strings" ) +type DiskConfig struct { + Path string + Dev string +} + type Config struct { - Name string - TapID int - Mac string - VolumePath string - Memory int - CPUs int + Name string + TapID int + Mac string + Disks []DiskConfig + Memory int + CPUs int UEFICodePath string UEFIVarsPath string - SerialDir string - MonitorDir string - QMPDir string + SerialDir string + MonitorDir string + QMPDir string } func Start(cfg Config) error { @@ -64,8 +71,36 @@ 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) + sort.Slice(sorted, func(i, j int) bool { return sorted[i].Dev < sorted[j].Dev }) + + for _, d := range sorted { + 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", d.Dev, scsiID), + ) + } else { + args = append(args, + "-drive", fmt.Sprintf("file=%s,if=virtio,id=%s", d.Path, d.Dev), + ) + } + } + 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), "-device", fmt.Sprintf("virtio-net-pci,netdev=net0,mac=%s", cfg.Mac), "-daemonize", diff --git a/internal/qemu/start_other.go b/internal/qemu/start_other.go index c9c9192..f48d95a 100644 --- a/internal/qemu/start_other.go +++ b/internal/qemu/start_other.go @@ -4,14 +4,23 @@ package qemu import "errors" +type DiskConfig struct { + Path string + Dev string +} + type Config struct { - Name, Mac, VolumePath string - TapID, Memory, CPUs int - UEFICodePath string - UEFIVarsPath string - SerialDir string - MonitorDir string - QMPDir string + Name string + TapID int + Mac string + Disks []DiskConfig + Memory int + CPUs int + UEFICodePath string + UEFIVarsPath string + SerialDir string + MonitorDir string + QMPDir string } func Start(_ Config) error { diff --git a/internal/vm/create.go b/internal/vm/create.go index 267dab5..3e0d4a7 100644 --- a/internal/vm/create.go +++ b/internal/vm/create.go @@ -52,11 +52,16 @@ func StartVM(db *badger.DB, name string, cfg *configuration.Config) error { 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{ Name: name, TapID: d.tapID, Mac: d.mac, - VolumePath: d.disks[0].path, + Disks: qDisks, Memory: d.memory, CPUs: d.cpus, SerialDir: cfg.QEMU.SerialDir, -- 2.49.1 From a0637d827ad16cda9457e8085221af29e59d205d Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 24 May 2026 16:13:11 +0200 Subject: [PATCH 3/6] f-32: syntax: fix duplicated code Signed-off-by: GnomeZworc --- internal/qemu/config.go | 20 ++++++++++++++++++++ internal/qemu/start_linux.go | 19 ------------------- internal/qemu/start_other.go | 23 +++-------------------- 3 files changed, 23 insertions(+), 39 deletions(-) create mode 100644 internal/qemu/config.go diff --git a/internal/qemu/config.go b/internal/qemu/config.go new file mode 100644 index 0000000..9b50add --- /dev/null +++ b/internal/qemu/config.go @@ -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 +} diff --git a/internal/qemu/start_linux.go b/internal/qemu/start_linux.go index 2043b64..ae03254 100644 --- a/internal/qemu/start_linux.go +++ b/internal/qemu/start_linux.go @@ -11,25 +11,6 @@ import ( "strings" ) -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 -} - func Start(cfg Config) error { memory := cfg.Memory if memory == 0 { diff --git a/internal/qemu/start_other.go b/internal/qemu/start_other.go index f48d95a..c28411c 100644 --- a/internal/qemu/start_other.go +++ b/internal/qemu/start_other.go @@ -2,26 +2,9 @@ package qemu -import "errors" - -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 -} +import ( + "errors" +) func Start(_ Config) error { return errors.New("vm: not supported on this platform") -- 2.49.1 From 9111045417e3a5bd1148d0fe2a0f1ef58d27df3c Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 24 May 2026 16:18:53 +0200 Subject: [PATCH 4/6] f-32: test: add test for vms Signed-off-by: GnomeZworc --- internal/api/agent/vm_test.go | 158 ++++++++++++++++++ internal/dispatcher/agent/vm_commands_test.go | 130 ++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 internal/api/agent/vm_test.go create mode 100644 internal/dispatcher/agent/vm_commands_test.go diff --git a/internal/api/agent/vm_test.go b/internal/api/agent/vm_test.go new file mode 100644 index 0000000..3af5968 --- /dev/null +++ b/internal/api/agent/vm_test.go @@ -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]) + } +} diff --git a/internal/dispatcher/agent/vm_commands_test.go b/internal/dispatcher/agent/vm_commands_test.go new file mode 100644 index 0000000..ba400b9 --- /dev/null +++ b/internal/dispatcher/agent/vm_commands_test.go @@ -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à") + } +} -- 2.49.1 From c3f26836cd60ab94bac2293ce767a8f4041cba41 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 24 May 2026 16:47:49 +0200 Subject: [PATCH 5/6] f-32: fix: update boot order Signed-off-by: GnomeZworc --- internal/qemu/start_linux.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/internal/qemu/start_linux.go b/internal/qemu/start_linux.go index ae03254..a7869a2 100644 --- a/internal/qemu/start_linux.go +++ b/internal/qemu/start_linux.go @@ -65,18 +65,29 @@ func Start(cfg Config) error { sorted := make([]DiskConfig, len(cfg.Disks)) copy(sorted, cfg.Disks) - sort.Slice(sorted, func(i, j int) bool { return sorted[i].Dev < sorted[j].Dev }) + // 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 _, d := range sorted { + 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", d.Dev, scsiID), + "-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=virtio,id=%s", d.Path, d.Dev), + "-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), ) } } -- 2.49.1 From 4ab880a32d8d4871a275af6fcc19bf58abfb2cfa Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 24 May 2026 19:02:31 +0200 Subject: [PATCH 6/6] f-32: vm: fix incomplete stop vm Signed-off-by: GnomeZworc --- internal/vm/delete.go | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/internal/vm/delete.go b/internal/vm/delete.go index 7e47f3f..3d808e6 100644 --- a/internal/vm/delete.go +++ b/internal/vm/delete.go @@ -33,25 +33,18 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error { socketPath := filepath.Join(cfg.QEMU.QMPDir, name+".sock") - if _, err := qmp.Send(socketPath, []string{`{"execute":"system_powerdown"}`}); err != nil { - return fmt.Errorf("qmp system_powerdown: %w", err) - } - - // attendre l'arrêt effectif de la VM ; forcer via quit après timeout - timeout := time.After(time.Duration(cfg.Dispatcher.TimeoutSeconds) * 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 - } + if _, err := os.Stat(socketPath); err == nil { + // socket présent : tenter l'arrêt gracieux + if _, err := qmp.Send(socketPath, []string{`{"execute":"system_powerdown"}`}); err == nil { + waitQMPDead(socketPath, + time.Duration(cfg.Dispatcher.TimeoutSeconds)*time.Second, + time.Duration(cfg.Dispatcher.PollSeconds)*time.Second, + ) } + // 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 { 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") } + +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 + } + } + } +} -- 2.49.1