Compare commits
14 commits
6abf93005b
...
14c5f41e2b
| Author | SHA1 | Date | |
|---|---|---|---|
|
14c5f41e2b |
|||
|
b67e79973c |
|||
|
50a290d808 |
|||
|
7b587f852e |
|||
|
686b2f62f3 |
|||
|
3323da955d |
|||
|
9980e03449 |
|||
|
b479493ac1 |
|||
|
7946129a75 |
|||
|
9e1e125d00 |
|||
|
8674f5d91d |
|||
|
d1ef83e949 |
|||
|
027ac11870 |
|||
|
cf8661ff7e |
15 changed files with 82 additions and 457 deletions
|
|
@ -87,11 +87,8 @@ 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}}
|
||||||
}
|
}
|
||||||
|
|
||||||
diskPrefix := prefix + "disk/"
|
if path := entries[prefix+"volume_path"]; path != "" {
|
||||||
for key, path := range entries {
|
vm.Storage = []VMStorage{{Path: path}}
|
||||||
if dev := strings.TrimPrefix(key, diskPrefix); dev != key {
|
|
||||||
vm.Storage = append(vm.Storage, VMStorage{Path: path, Dev: dev})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return vm, nil
|
return vm, nil
|
||||||
|
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
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])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -77,21 +77,16 @@ 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,
|
||||||
Disks: disks,
|
VolumePath: req.Storage[0].Path,
|
||||||
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 {
|
||||||
|
|
|
||||||
|
|
@ -13,21 +13,16 @@ 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
|
||||||
Disks []VMDisk
|
VolumePath string
|
||||||
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 {
|
||||||
|
|
@ -49,9 +44,7 @@ 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))
|
||||||
for _, d := range c.Disks {
|
kv.AddInDB(db, "vm/"+c.Name+"/volume_path", c.VolumePath)
|
||||||
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 {
|
||||||
|
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
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à")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -7,10 +7,22 @@ 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 {
|
||||||
|
|
@ -52,47 +64,8 @@ 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",
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,17 @@
|
||||||
|
|
||||||
package qemu
|
package qemu
|
||||||
|
|
||||||
import (
|
import "errors"
|
||||||
"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")
|
||||||
|
|
|
||||||
|
|
@ -52,16 +52,11 @@ 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,
|
||||||
Disks: qDisks,
|
VolumePath: d.volumePath,
|
||||||
Memory: d.memory,
|
Memory: d.memory,
|
||||||
CPUs: d.cpus,
|
CPUs: d.cpus,
|
||||||
SerialDir: cfg.QEMU.SerialDir,
|
SerialDir: cfg.QEMU.SerialDir,
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,6 @@ 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
|
||||||
|
|
@ -25,7 +20,7 @@ type vmData struct {
|
||||||
ip string
|
ip string
|
||||||
metadataPort string
|
metadataPort string
|
||||||
mac string
|
mac string
|
||||||
disks []diskEntry
|
volumePath string
|
||||||
memory int
|
memory int
|
||||||
cpus int
|
cpus int
|
||||||
uefi bool
|
uefi bool
|
||||||
|
|
@ -87,18 +82,11 @@ func loadVM(db *badger.DB, name string) (vmData, error) {
|
||||||
}
|
}
|
||||||
d.mac = mac
|
d.mac = mac
|
||||||
|
|
||||||
diskEntries, err := kv.ListByPrefix(db, "vm/"+name+"/disk/")
|
volumePath, err := kv.GetFromDB(db, "vm/"+name+"/volume_path")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return d, fmt.Errorf("list disks: %w", err)
|
return d, fmt.Errorf("get volume_path: %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 {
|
||||||
|
|
|
||||||
|
|
@ -33,18 +33,25 @@ 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 := os.Stat(socketPath); err == nil {
|
if _, err := qmp.Send(socketPath, []string{`{"execute":"system_powerdown"}`}); err != nil {
|
||||||
// socket présent : tenter l'arrêt gracieux
|
return fmt.Errorf("qmp system_powerdown: %w", err)
|
||||||
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
|
|
||||||
|
|
||||||
|
// 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 := 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)
|
||||||
|
|
@ -67,18 +74,3 @@ 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
[
|
[]
|
||||||
]
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
local_components:
|
local_components:
|
||||||
- login-gate
|
- login-gate
|
||||||
- api-client
|
- api-client
|
||||||
|
- side-nav
|
||||||
- logout-button
|
- logout-button
|
||||||
- side-panel
|
- side-panel
|
||||||
- date-display
|
- date-display
|
||||||
|
|
@ -24,10 +25,7 @@ local_components:
|
||||||
# - name: vpc-panel # → components/vpc-panel/
|
# - name: vpc-panel # → components/vpc-panel/
|
||||||
# repo: https://git.g3e.fr/team-reseau/vpc-panel
|
# repo: https://git.g3e.fr/team-reseau/vpc-panel
|
||||||
# ref: v1.2.0 # tag, branch or commit (default: main)
|
# ref: v1.2.0 # tag, branch or commit (default: main)
|
||||||
components:
|
components: []
|
||||||
- name: side-nav
|
|
||||||
repo: https://git.g3e.fr/syonad/syn-side-nav
|
|
||||||
ref: main
|
|
||||||
|
|
||||||
# Vendor libraries — downloaded into web/vendor/ at build time (gitignored).
|
# Vendor libraries — downloaded into web/vendor/ at build time (gitignored).
|
||||||
# Components import them via relative paths, never via components.json.
|
# Components import them via relative paths, never via components.json.
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,19 @@ const CHEVRON_ICON = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none
|
||||||
<polyline points="9 18 15 12 9 6"/>
|
<polyline points="9 18 15 12 9 6"/>
|
||||||
</svg>`
|
</svg>`
|
||||||
|
|
||||||
const CSS = new URL('./side-nav.css', import.meta.url).href
|
const ICONS = {
|
||||||
const MQ = window.matchMedia('(max-width: 768px)')
|
dashboard: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>`,
|
||||||
const NAV_W = 220 // px
|
vpc: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5"/><path d="M3 12c0 1.66 4.03 3 9 3s9-1.34 9-3"/></svg>`,
|
||||||
|
subnet: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="2"/><path d="M8 12h8M12 8v8"/></svg>`,
|
||||||
|
vm: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>`,
|
||||||
|
}
|
||||||
|
|
||||||
const ICON_FALLBACK = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/></svg>`
|
const CSS = new URL('./side-nav.css', import.meta.url).href
|
||||||
|
const MQ = window.matchMedia('(max-width: 768px)')
|
||||||
const iconsRes = await fetch('./icons.json')
|
const NAV_W = 220 // px
|
||||||
const ICONS = iconsRes.ok ? await iconsRes.json() : {}
|
|
||||||
|
|
||||||
function resolveIcon(name) {
|
function resolveIcon(name) {
|
||||||
return ICONS[name] ?? ICON_FALLBACK
|
return ICONS[name] ?? `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/></svg>`
|
||||||
}
|
}
|
||||||
|
|
||||||
class SideNav extends HTMLElement {
|
class SideNav extends HTMLElement {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"dashboard": "<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"7\" height=\"7\"/><rect x=\"14\" y=\"3\" width=\"7\" height=\"7\"/><rect x=\"14\" y=\"14\" width=\"7\" height=\"7\"/><rect x=\"3\" y=\"14\" width=\"7\" height=\"7\"/></svg>",
|
|
||||||
"vpc": "<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><ellipse cx=\"12\" cy=\"5\" rx=\"9\" ry=\"3\"/><path d=\"M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5\"/><path d=\"M3 12c0 1.66 4.03 3 9 3s9-1.34 9-3\"/></svg>",
|
|
||||||
"subnet": "<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"2\" width=\"20\" height=\"20\" rx=\"2\"/><path d=\"M8 12h8M12 8v8\"/></svg>",
|
|
||||||
"vm": "<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"3\" width=\"20\" height=\"14\" rx=\"2\"/><path d=\"M8 21h8M12 17v4\"/></svg>",
|
|
||||||
"account": "<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"8\" r=\"4\"/><path d=\"M4 20c0-4 3.6-7 8-7s8 3 8 7\"/></svg>"
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue