diff --git a/api/agent.yaml b/api/agent.yaml index e75601d..7e9c461 100644 --- a/api/agent.yaml +++ b/api/agent.yaml @@ -430,6 +430,10 @@ components: minItems: 1 items: $ref: "#/components/schemas/VMStorage" + uefi: + type: boolean + description: Boot with UEFI firmware (OVMF). Defaults to false (SeaBIOS). + example: false VMInterface: type: object @@ -487,6 +491,9 @@ components: type: array items: $ref: "#/components/schemas/VMStorage" + uefi: + type: boolean + example: false Error: type: object diff --git a/conf/agent/config.exemple.yml b/conf/agent/config.exemple.yml index fdef682..0517eac 100644 --- a/conf/agent/config.exemple.yml +++ b/conf/agent/config.exemple.yml @@ -39,6 +39,18 @@ interfaces: metadata: run_dir: "/run/two/metadata" +# QEMU runtime paths +qemu: + # UEFI firmware (requires apt install ovmf on Debian/Ubuntu) + ovmf_code_path: "/usr/share/OVMF/OVMF_CODE.fd" + ovmf_vars_template: "/usr/share/OVMF/OVMF_VARS.fd" + # Per-VM UEFI variable store (writable copy, created at start / deleted at stop) + uefi_vars_dir: "/run/two/vms/efi" + # QEMU Unix socket directories + serial_dir: "/run/two/vms/serial" + monitor_dir: "/run/two/vms/monitor" + qmp_dir: "/run/two/vms/qmp" + # Admin API (read-only DB inspection, loopback only) admin: enabled: false diff --git a/internal/api/agent/models.go b/internal/api/agent/models.go index dc0c47f..079d8db 100644 --- a/internal/api/agent/models.go +++ b/internal/api/agent/models.go @@ -49,6 +49,7 @@ type VMCreateRequest struct { Name string `json:"name"` Memory int `json:"memory"` CPUs int `json:"cpus"` + UEFI bool `json:"uefi"` Password string `json:"password"` SSHKey string `json:"sshkey"` Interfaces []VMInterface `json:"interfaces"` @@ -61,6 +62,7 @@ type VM struct { MetadataPort string `json:"metadata_port"` Memory int `json:"memory"` CPUs int `json:"cpus"` + UEFI bool `json:"uefi"` Interfaces []VMInterface `json:"interfaces"` Storage []VMStorage `json:"storage"` } diff --git a/internal/api/agent/vm.go b/internal/api/agent/vm.go index a150d16..f932766 100644 --- a/internal/api/agent/vm.go +++ b/internal/api/agent/vm.go @@ -79,6 +79,7 @@ func vmFromDB(name string, entries map[string]string) (VM, error) { vm.MetadataPort = entries[prefix+"metadata_port"] vm.Memory, _ = strconv.Atoi(entries[prefix+"memory"]) vm.CPUs, _ = strconv.Atoi(entries[prefix+"cpus"]) + vm.UEFI = entries[prefix+"uefi"] == "true" subnet := entries[prefix+"subnet"] ip := entries[prefix+"ip"] diff --git a/internal/api/agent/vms.go b/internal/api/agent/vms.go index 95f2c85..4dc6136 100644 --- a/internal/api/agent/vms.go +++ b/internal/api/agent/vms.go @@ -84,6 +84,7 @@ func (s *Server) startVM(w http.ResponseWriter, r *http.Request) { VolumePath: req.Storage[0].Path, Memory: req.Memory, CPUs: req.CPUs, + UEFI: req.UEFI, Password: req.Password, SSHKey: req.SSHKey, } diff --git a/internal/config/agent/struct.go b/internal/config/agent/struct.go index 9889c79..8b5d406 100644 --- a/internal/config/agent/struct.go +++ b/internal/config/agent/struct.go @@ -36,6 +36,14 @@ type Config struct { Address string `mapstructure:"address"` Port int `mapstructure:"port"` } `mapstructure:"admin"` + QEMU struct { + OVMFCodePath string `mapstructure:"ovmf_code_path"` + OVMFVarsTemplate string `mapstructure:"ovmf_vars_template"` + UEFIVarsDir string `mapstructure:"uefi_vars_dir"` + SerialDir string `mapstructure:"serial_dir"` + MonitorDir string `mapstructure:"monitor_dir"` + QMPDir string `mapstructure:"qmp_dir"` + } `mapstructure:"qemu"` DefaultInterface string `mapstructure:"default_interface"` Interfaces map[string]string `mapstructure:"interfaces"` } @@ -55,6 +63,12 @@ func LoadConfig(path string) (*Config, error) { v.SetDefault("dispatcher.timeout_seconds", 300) v.SetDefault("dispatcher.poll_seconds", 2) v.SetDefault("metadata.run_dir", "/run/two/metadata") + v.SetDefault("qemu.ovmf_code_path", "/usr/share/OVMF/OVMF_CODE.fd") + v.SetDefault("qemu.ovmf_vars_template", "/usr/share/OVMF/OVMF_VARS.fd") + v.SetDefault("qemu.uefi_vars_dir", "/run/two/vms/uefi") + v.SetDefault("qemu.serial_dir", "/run/two/vms/serial") + v.SetDefault("qemu.monitor_dir", "/run/two/vms/monitor") + v.SetDefault("qemu.qmp_dir", "/run/two/vms/qmp") v.SetDefault("admin.enabled", false) v.SetDefault("admin.address", "127.0.0.1") v.SetDefault("admin.port", 9091) diff --git a/internal/dispatcher/agent/vm_commands.go b/internal/dispatcher/agent/vm_commands.go index 1bb55d4..1c4bbce 100644 --- a/internal/dispatcher/agent/vm_commands.go +++ b/internal/dispatcher/agent/vm_commands.go @@ -20,6 +20,7 @@ type StartVMCommand struct { VolumePath string Memory int CPUs int + UEFI bool Password string SSHKey string } @@ -46,6 +47,9 @@ func (c StartVMCommand) Prepare(db *badger.DB, _ *configuration.Config) error { kv.AddInDB(db, "vm/"+c.Name+"/volume_path", c.VolumePath) 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 { + kv.AddInDB(db, "vm/"+c.Name+"/uefi", "true") + } if c.Password != "" { kv.AddInDB(db, "vm/"+c.Name+"/password", c.Password) } diff --git a/internal/qemu/start_linux.go b/internal/qemu/start_linux.go index 6119278..3f40e73 100644 --- a/internal/qemu/start_linux.go +++ b/internal/qemu/start_linux.go @@ -4,7 +4,9 @@ package qemu import ( "fmt" + "os" "os/exec" + "path/filepath" ) type Config struct { @@ -14,6 +16,11 @@ type Config struct { VolumePath string Memory int CPUs int + UEFICodePath string + UEFIVarsPath string + SerialDir string + MonitorDir string + QMPDir string } func Start(cfg Config) error { @@ -27,20 +34,44 @@ func Start(cfg Config) error { cpus = 1 } - cmd := exec.Command("qemu-system-x86_64", + for _, dir := range []string{cfg.SerialDir, cfg.MonitorDir, cfg.QMPDir} { + if dir != "" { + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("mkdir %s: %w", dir, err) + } + } + } + + serialSock := filepath.Join(cfg.SerialDir, cfg.Name+".sock") + monitorSock := filepath.Join(cfg.MonitorDir, cfg.Name+".sock") + qmpSock := filepath.Join(cfg.QMPDir, cfg.Name+".sock") + + args := []string{ "-enable-kvm", "-cpu", "host", "-m", fmt.Sprintf("%d", memory), "-smp", fmt.Sprintf("%d", cpus), - "-serial", fmt.Sprintf("unix:/tmp/%s.sock,server,nowait", cfg.Name), - "-monitor", fmt.Sprintf("unix:/tmp/%s.mon-sock,server,nowait", cfg.Name), - "-qmp", fmt.Sprintf("unix:/tmp/%s.qmp-sock,server,nowait", cfg.Name), + "-serial", fmt.Sprintf("unix:%s,server,nowait", serialSock), + "-monitor", fmt.Sprintf("unix:%s,server,nowait", monitorSock), + "-qmp", fmt.Sprintf("unix:%s,server,nowait", qmpSock), "-display", "none", + } + + if cfg.UEFICodePath != "" && cfg.UEFIVarsPath != "" { + args = append(args, + "-drive", fmt.Sprintf("if=pflash,format=raw,readonly=on,file=%s", cfg.UEFICodePath), + "-drive", fmt.Sprintf("if=pflash,format=raw,file=%s", cfg.UEFIVarsPath), + ) + } + + 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", ) + + cmd := exec.Command("qemu-system-x86_64", args...) if err := cmd.Run(); err != nil { return fmt.Errorf("qemu-system-x86_64: %w", err) } diff --git a/internal/qemu/start_other.go b/internal/qemu/start_other.go index 782a7ee..c9c9192 100644 --- a/internal/qemu/start_other.go +++ b/internal/qemu/start_other.go @@ -7,6 +7,11 @@ 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 { diff --git a/internal/vm/create.go b/internal/vm/create.go index f3e9d34..529c73c 100644 --- a/internal/vm/create.go +++ b/internal/vm/create.go @@ -2,6 +2,9 @@ package vm import ( "fmt" + "io" + "os" + "path/filepath" configuration "git.g3e.fr/syonad/two/internal/config/agent" "git.g3e.fr/syonad/two/internal/iptables" @@ -49,18 +52,54 @@ func StartVM(db *badger.DB, name string, cfg *configuration.Config) error { return fmt.Errorf("start metadata: %w", err) } + qcfg := qemu.Config{ + Name: name, + TapID: d.tapID, + Mac: d.mac, + VolumePath: d.volumePath, + Memory: d.memory, + CPUs: d.cpus, + SerialDir: cfg.QEMU.SerialDir, + MonitorDir: cfg.QEMU.MonitorDir, + QMPDir: cfg.QEMU.QMPDir, + } + + if d.uefi { + varsPath := filepath.Join(cfg.QEMU.UEFIVarsDir, name+"-uefi-vars.fd") + if err := copyFile(cfg.QEMU.OVMFVarsTemplate, varsPath); err != nil { + return fmt.Errorf("copy uefi vars: %w", err) + } + qcfg.UEFICodePath = cfg.QEMU.OVMFCodePath + qcfg.UEFIVarsPath = varsPath + } + if err := netns.Call(d.vpcName, func() error { - return qemu.Start(qemu.Config{ - Name: name, - TapID: d.tapID, - Mac: d.mac, - VolumePath: d.volumePath, - Memory: d.memory, - CPUs: d.cpus, - }) + return qemu.Start(qcfg) }); err != nil { return fmt.Errorf("start qemu: %w", err) } return kv.AddInDB(db, "vm/"+name+"/state", "started") } + +func copyFile(src, dst string) error { + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return err + } + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + if _, err := io.Copy(out, in); err != nil { + return err + } + return out.Sync() +} diff --git a/internal/vm/data.go b/internal/vm/data.go index df5c051..1e2644e 100644 --- a/internal/vm/data.go +++ b/internal/vm/data.go @@ -14,7 +14,7 @@ import ( type vmData struct { subnetName string vpcName string - interfaceIP string + interfaceIP string bridge string tapID int ip string @@ -23,6 +23,7 @@ type vmData struct { volumePath string memory int cpus int + uefi bool password string sshkey string } @@ -105,6 +106,9 @@ func loadVM(db *badger.DB, name string) (vmData, error) { return d, fmt.Errorf("parse cpus: %w", err) } + if v, _ := kv.GetFromDB(db, "vm/"+name+"/uefi"); v == "true" { + d.uefi = true + } d.password, _ = kv.GetFromDB(db, "vm/"+name+"/password") d.sshkey, _ = kv.GetFromDB(db, "vm/"+name+"/sshkey") diff --git a/internal/vm/delete.go b/internal/vm/delete.go index 79a4c72..7e47f3f 100644 --- a/internal/vm/delete.go +++ b/internal/vm/delete.go @@ -2,6 +2,8 @@ package vm import ( "fmt" + "os" + "path/filepath" "time" configuration "git.g3e.fr/syonad/two/internal/config/agent" @@ -29,7 +31,7 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error { return err } - socketPath := fmt.Sprintf("/tmp/%s.qmp-sock", name) + 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) @@ -65,5 +67,10 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error { return fmt.Errorf("delete tap: %w", err) } + if d.uefi { + varsPath := filepath.Join(cfg.QEMU.UEFIVarsDir, name+"-uefi-vars.fd") + os.Remove(varsPath) + } + return kv.AddInDB(db, "vm/"+name+"/state", "stopped") }