Compare commits
12 commits
895d03d362
...
7078691db6
| Author | SHA1 | Date | |
|---|---|---|---|
|
7078691db6 |
|||
|
593bd42e6a |
|||
|
1ea3a986b8 |
|||
|
c17ee79183 |
|||
|
f005d9dde7 |
|||
|
b4092fbe02 |
|||
|
371ec75468 |
|||
|
cc9998028d |
|||
|
2efaf20098 |
|||
|
504a2a723b |
|||
|
a5bf748bdf |
|||
|
da0d043846 |
10 changed files with 553 additions and 36 deletions
180
api/agent.yaml
180
api/agent.yaml
|
|
@ -99,6 +99,95 @@ paths:
|
|||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
# ── VM ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/vms:
|
||||
get:
|
||||
summary: List all VMs
|
||||
operationId: listVMs
|
||||
responses:
|
||||
"200":
|
||||
description: List of VMs
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/VM"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
post:
|
||||
summary: Start a VM
|
||||
operationId: startVM
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/VMCreateRequest"
|
||||
responses:
|
||||
"202":
|
||||
description: VM start accepted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/VM"
|
||||
"400":
|
||||
description: Missing required field or invalid request body
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"409":
|
||||
description: VM already exists
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"422":
|
||||
description: Subnet not found or not in created state
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
/vms/{name}:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ResourceName"
|
||||
|
||||
get:
|
||||
summary: Get VM status and info
|
||||
operationId: getVM
|
||||
responses:
|
||||
"200":
|
||||
description: VM found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/VM"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
delete:
|
||||
summary: Stop a VM
|
||||
operationId: stopVM
|
||||
responses:
|
||||
"202":
|
||||
description: VM stop accepted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/VM"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
# ── Subnet ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/subnets:
|
||||
|
|
@ -281,6 +370,97 @@ components:
|
|||
type: string
|
||||
example: "10.10.10.0/24"
|
||||
|
||||
VMCreateRequest:
|
||||
type: object
|
||||
required: [name, metadata_port, interfaces, storage]
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: vm-00001
|
||||
metadata_port:
|
||||
type: string
|
||||
example: "80"
|
||||
memory:
|
||||
type: integer
|
||||
description: Memory in MB (default 512)
|
||||
example: 1024
|
||||
cpus:
|
||||
type: integer
|
||||
description: Number of vCPUs (default 1)
|
||||
example: 2
|
||||
password:
|
||||
type: string
|
||||
sshkey:
|
||||
type: string
|
||||
example: "ssh-ed25519 AAAA..."
|
||||
interfaces:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
$ref: "#/components/schemas/VMInterface"
|
||||
storage:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
$ref: "#/components/schemas/VMStorage"
|
||||
|
||||
VMInterface:
|
||||
type: object
|
||||
required: [subnet, ip, primary]
|
||||
properties:
|
||||
subnet:
|
||||
type: string
|
||||
example: sn-00001
|
||||
ip:
|
||||
type: string
|
||||
format: ipv4
|
||||
example: "10.0.0.5"
|
||||
primary:
|
||||
type: boolean
|
||||
example: true
|
||||
|
||||
VMStorage:
|
||||
type: object
|
||||
required: [path, dev]
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: Path to the disk image on the host
|
||||
example: /var/lib/two/volumes/abc.qcow2
|
||||
dev:
|
||||
type: string
|
||||
description: Device name inside the VM
|
||||
pattern: '^[sv]d[a-z]$'
|
||||
example: vda
|
||||
|
||||
VM:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: vm-00001
|
||||
state:
|
||||
type: string
|
||||
enum: [starting, started, stopping, stopped]
|
||||
example: started
|
||||
metadata_port:
|
||||
type: string
|
||||
example: "80"
|
||||
memory:
|
||||
type: integer
|
||||
example: 1024
|
||||
cpus:
|
||||
type: integer
|
||||
example: 2
|
||||
interfaces:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/VMInterface"
|
||||
storage:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/VMStorage"
|
||||
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
|||
|
|
@ -28,6 +28,38 @@ type Subnet struct {
|
|||
CIDR string `json:"cidr"`
|
||||
}
|
||||
|
||||
type VMInterface struct {
|
||||
Subnet string `json:"subnet"`
|
||||
IP string `json:"ip"`
|
||||
Primary bool `json:"primary"`
|
||||
}
|
||||
|
||||
type VMStorage struct {
|
||||
Path string `json:"path"`
|
||||
Dev string `json:"dev"`
|
||||
}
|
||||
|
||||
type VMCreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
MetadataPort string `json:"metadata_port"`
|
||||
Memory int `json:"memory"`
|
||||
CPUs int `json:"cpus"`
|
||||
Password string `json:"password"`
|
||||
SSHKey string `json:"sshkey"`
|
||||
Interfaces []VMInterface `json:"interfaces"`
|
||||
Storage []VMStorage `json:"storage"`
|
||||
}
|
||||
|
||||
type VM struct {
|
||||
Name string `json:"name"`
|
||||
State string `json:"state"`
|
||||
MetadataPort string `json:"metadata_port"`
|
||||
Memory int `json:"memory"`
|
||||
CPUs int `json:"cpus"`
|
||||
Interfaces []VMInterface `json:"interfaces"`
|
||||
Storage []VMStorage `json:"storage"`
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ func (s *Server) Start(address string) {
|
|||
mux.HandleFunc("/vpcs/", s.VpcByNameHandler)
|
||||
mux.HandleFunc("/subnets", s.SubnetsHandler)
|
||||
mux.HandleFunc("/subnets/", s.SubnetByNameHandler)
|
||||
mux.HandleFunc("/vms", s.VmsHandler)
|
||||
mux.HandleFunc("/vms/", s.VmByNameHandler)
|
||||
s.logger.Info("API server listening", "address", address)
|
||||
if err := http.ListenAndServe(address, s.logMiddleware(mux)); err != nil {
|
||||
s.logger.Error("API server stopped", "error", err)
|
||||
|
|
|
|||
94
internal/api/agent/vm.go
Normal file
94
internal/api/agent/vm.go
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
package agentapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
dispatcher "git.g3e.fr/syonad/two/internal/dispatcher/agent"
|
||||
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||
)
|
||||
|
||||
func (s *Server) VmByNameHandler(w http.ResponseWriter, r *http.Request) {
|
||||
name := strings.TrimPrefix(r.URL.Path, "/vms/")
|
||||
if name == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "resource not found"})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
s.getVM(w, r, name)
|
||||
case http.MethodDelete:
|
||||
s.stopVM(w, r, name)
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "method not allowed"})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) getVM(w http.ResponseWriter, _ *http.Request, name string) {
|
||||
entries, err := kv.ListByPrefix(s.db, "vm/"+name+"/")
|
||||
if err != nil || len(entries) == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "vm not found"})
|
||||
return
|
||||
}
|
||||
vm, err := vmFromDB(name, entries)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "failed to read vm"})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(vm)
|
||||
}
|
||||
|
||||
func (s *Server) stopVM(w http.ResponseWriter, _ *http.Request, name string) {
|
||||
cmd := dispatcher.StopVMCommand{Name: name}
|
||||
if err := s.dispatcher.Prepare(cmd); err != nil {
|
||||
if _, dbErr := kv.GetFromDB(s.db, "vm/"+name+"/state"); dbErr != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
}
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
s.dispatcher.Dispatch(cmd)
|
||||
|
||||
entries, _ := kv.ListByPrefix(s.db, "vm/"+name+"/")
|
||||
vm, err := vmFromDB(name, entries)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "failed to read vm state"})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
json.NewEncoder(w).Encode(vm)
|
||||
}
|
||||
|
||||
func vmFromDB(name string, entries map[string]string) (VM, error) {
|
||||
prefix := "vm/" + name + "/"
|
||||
vm := VM{Name: name}
|
||||
|
||||
vm.State = entries[prefix+"state"]
|
||||
vm.MetadataPort = entries[prefix+"metadata_port"]
|
||||
vm.Memory, _ = strconv.Atoi(entries[prefix+"memory"])
|
||||
vm.CPUs, _ = strconv.Atoi(entries[prefix+"cpus"])
|
||||
|
||||
subnet := entries[prefix+"subnet"]
|
||||
ip := entries[prefix+"ip"]
|
||||
if subnet != "" || ip != "" {
|
||||
vm.Interfaces = []VMInterface{{Subnet: subnet, IP: ip, Primary: true}}
|
||||
}
|
||||
|
||||
if path := entries[prefix+"volume_path"]; path != "" {
|
||||
vm.Storage = []VMStorage{{Path: path}}
|
||||
}
|
||||
|
||||
return vm, nil
|
||||
}
|
||||
112
internal/api/agent/vms.go
Normal file
112
internal/api/agent/vms.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
package agentapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
dispatcher "git.g3e.fr/syonad/two/internal/dispatcher/agent"
|
||||
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||
)
|
||||
|
||||
func (s *Server) VmsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
s.listVMs(w, r)
|
||||
case http.MethodPost:
|
||||
s.startVM(w, r)
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "method not allowed"})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) listVMs(w http.ResponseWriter, _ *http.Request) {
|
||||
entries, err := kv.ListByPrefix(s.db, "vm/")
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "failed to list vms"})
|
||||
return
|
||||
}
|
||||
|
||||
names := map[string]struct{}{}
|
||||
for key := range entries {
|
||||
parts := strings.Split(key, "/")
|
||||
if len(parts) >= 2 {
|
||||
names[parts[1]] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]VM, 0, len(names))
|
||||
for name := range names {
|
||||
vm, err := vmFromDB(name, entries)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, vm)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
func (s *Server) startVM(w http.ResponseWriter, r *http.Request) {
|
||||
var req VMCreateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "invalid request body"})
|
||||
return
|
||||
}
|
||||
if req.Name == "" || req.MetadataPort == "" || len(req.Interfaces) == 0 || len(req.Storage) == 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "name, metadata_port, interfaces and storage are required"})
|
||||
return
|
||||
}
|
||||
|
||||
var primary *VMInterface
|
||||
for i := range req.Interfaces {
|
||||
if req.Interfaces[i].Primary {
|
||||
primary = &req.Interfaces[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if primary == nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "one interface must be primary"})
|
||||
return
|
||||
}
|
||||
|
||||
cmd := dispatcher.StartVMCommand{
|
||||
Name: req.Name,
|
||||
Subnet: primary.Subnet,
|
||||
IP: primary.IP,
|
||||
MetadataPort: req.MetadataPort,
|
||||
VolumePath: req.Storage[0].Path,
|
||||
Memory: req.Memory,
|
||||
CPUs: req.CPUs,
|
||||
Password: req.Password,
|
||||
SSHKey: req.SSHKey,
|
||||
}
|
||||
|
||||
if err := s.dispatcher.Prepare(cmd); err != nil {
|
||||
if _, dbErr := kv.GetFromDB(s.db, "vm/"+req.Name+"/state"); dbErr == nil {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
}
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
s.dispatcher.Dispatch(cmd)
|
||||
|
||||
entries, _ := kv.ListByPrefix(s.db, "vm/"+req.Name+"/")
|
||||
vm, err := vmFromDB(req.Name, entries)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Error: "failed to read vm state"})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
json.NewEncoder(w).Encode(vm)
|
||||
}
|
||||
95
internal/dispatcher/agent/vm_commands.go
Normal file
95
internal/dispatcher/agent/vm_commands.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package dispatcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
||||
"git.g3e.fr/syonad/two/internal/vm"
|
||||
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
)
|
||||
|
||||
type StartVMCommand struct {
|
||||
Name string
|
||||
Subnet string
|
||||
IP string
|
||||
MetadataPort string
|
||||
VolumePath string
|
||||
Memory int
|
||||
CPUs int
|
||||
Password string
|
||||
SSHKey string
|
||||
}
|
||||
|
||||
func (c StartVMCommand) Prepare(db *badger.DB, _ *configuration.Config) error {
|
||||
if _, err := kv.GetFromDB(db, "vm/"+c.Name+"/state"); err == nil {
|
||||
return fmt.Errorf("vm %q already exists", c.Name)
|
||||
}
|
||||
subnetState, err := kv.GetFromDB(db, "subnet/"+c.Subnet+"/state")
|
||||
if err != nil {
|
||||
return fmt.Errorf("subnet %q not found", c.Subnet)
|
||||
}
|
||||
if subnetState == "deleting" || subnetState == "deleted" {
|
||||
return fmt.Errorf("subnet %q is %s", c.Subnet, subnetState)
|
||||
}
|
||||
kv.AddInDB(db, "vm/"+c.Name+"/state", "starting")
|
||||
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", c.MetadataPort)
|
||||
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.Password != "" {
|
||||
kv.AddInDB(db, "vm/"+c.Name+"/password", c.Password)
|
||||
}
|
||||
if c.SSHKey != "" {
|
||||
kv.AddInDB(db, "vm/"+c.Name+"/sshkey", c.SSHKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c StartVMCommand) Execute(db *badger.DB, cfg *configuration.Config) error {
|
||||
timeout := time.After(time.Duration(cfg.Dispatcher.TimeoutSeconds) * time.Second)
|
||||
for {
|
||||
state, err := kv.GetFromDB(db, "subnet/"+c.Subnet+"/state")
|
||||
if err != nil {
|
||||
return fmt.Errorf("subnet %q not found while waiting", c.Subnet)
|
||||
}
|
||||
if state == "created" {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-timeout:
|
||||
return fmt.Errorf("timed out waiting for subnet %q to be created", c.Subnet)
|
||||
case <-time.After(time.Duration(cfg.Dispatcher.PollSeconds) * time.Second):
|
||||
}
|
||||
}
|
||||
return vm.StartVM(db, c.Name)
|
||||
}
|
||||
|
||||
type StopVMCommand struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (c StopVMCommand) Prepare(db *badger.DB, _ *configuration.Config) error {
|
||||
if _, err := kv.GetFromDB(db, "vm/"+c.Name+"/state"); err != nil {
|
||||
return fmt.Errorf("vm %q not found", c.Name)
|
||||
}
|
||||
return kv.AddInDB(db, "vm/"+c.Name+"/state", "stopping")
|
||||
}
|
||||
|
||||
func (c StopVMCommand) Execute(db *badger.DB, cfg *configuration.Config) error {
|
||||
if err := vm.StopVM(db, c.Name, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
state, err := kv.GetFromDB(db, "vm/"+c.Name+"/state")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if state == "stopped" {
|
||||
kv.DeleteInDB(db, "vm/"+c.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -5,13 +5,10 @@ package qemu
|
|||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"git.g3e.fr/syonad/two/internal/netns"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Name string
|
||||
VpcName string
|
||||
TapID int
|
||||
Mac string
|
||||
VolumePath string
|
||||
|
|
@ -30,24 +27,22 @@ func Start(cfg Config) error {
|
|||
cpus = 1
|
||||
}
|
||||
|
||||
return netns.Call(cfg.VpcName, func() error {
|
||||
cmd := exec.Command("qemu-system-x86_64",
|
||||
"-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),
|
||||
"-display", "none",
|
||||
"-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",
|
||||
)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("qemu-system-x86_64: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
cmd := exec.Command("qemu-system-x86_64",
|
||||
"-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),
|
||||
"-display", "none",
|
||||
"-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",
|
||||
)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("qemu-system-x86_64: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ package qemu
|
|||
import "errors"
|
||||
|
||||
type Config struct {
|
||||
Name, VpcName, Mac, VolumePath string
|
||||
TapID, Memory, CPUs int
|
||||
Name, Mac, VolumePath string
|
||||
TapID, Memory, CPUs int
|
||||
}
|
||||
|
||||
func Start(cfg Config) error {
|
||||
func Start(_ Config) error {
|
||||
return errors.New("vm: not supported on this platform")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,14 +48,15 @@ func StartVM(db *badger.DB, name string) error {
|
|||
return fmt.Errorf("start metadata: %w", err)
|
||||
}
|
||||
|
||||
if err := qemu.Start(qemu.Config{
|
||||
Name: name,
|
||||
VpcName: d.vpcName,
|
||||
TapID: d.tapID,
|
||||
Mac: d.mac,
|
||||
VolumePath: d.volumePath,
|
||||
Memory: d.memory,
|
||||
CPUs: d.cpus,
|
||||
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,
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("start qemu: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,13 +91,19 @@ func loadVM(db *badger.DB, name string) (vmData, error) {
|
|||
if err != nil {
|
||||
return d, fmt.Errorf("get memory: %w", err)
|
||||
}
|
||||
d.memory, _ = strconv.Atoi(memoryStr)
|
||||
d.memory, err = strconv.Atoi(memoryStr)
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("parse memory: %w", err)
|
||||
}
|
||||
|
||||
cpusStr, err := kv.GetFromDB(db, "vm/"+name+"/cpus")
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("get cpus: %w", err)
|
||||
}
|
||||
d.cpus, _ = strconv.Atoi(cpusStr)
|
||||
d.cpus, err = strconv.Atoi(cpusStr)
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("parse cpus: %w", err)
|
||||
}
|
||||
|
||||
d.password, _ = kv.GetFromDB(db, "vm/"+name+"/password")
|
||||
d.sshkey, _ = kv.GetFromDB(db, "vm/"+name+"/sshkey")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue