Merge branch 'feature-25'
This commit is contained in:
commit
7065a9f431
34 changed files with 1295 additions and 330 deletions
|
|
@ -33,9 +33,7 @@ jobs:
|
|||
goos: [linux]
|
||||
goarch: [amd64]
|
||||
binaries:
|
||||
- db
|
||||
- metadata
|
||||
- metacli
|
||||
- agent
|
||||
uses: ./.forgejo/workflows/build.yml
|
||||
with:
|
||||
|
|
|
|||
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:
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ func main() {
|
|||
d := dispatcher.New(q, db, cfg, log.With(slog.String("component", "dispatcher")))
|
||||
go agentapi.New(d, db, log.With(slog.String("component", "api"))).Start(apiAddr)
|
||||
go promserver.Start(promAddr, registry)
|
||||
if cfg.Admin.Enabled {
|
||||
adminAddr := fmt.Sprintf("%s:%d", cfg.Admin.Address, cfg.Admin.Port)
|
||||
go kv.NewAdminServer(db, log.With(slog.String("component", "admin"))).Start(adminAddr)
|
||||
}
|
||||
|
||||
select {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
||||
"git.g3e.fr/syonad/two/internal/metadata"
|
||||
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
conf_file := flag.String("conf", "/etc/two/agent.yml", "configuration file")
|
||||
vm_name := flag.String("vm_name", "", "Nom de la vm")
|
||||
vpc := flag.String("vpc_name", "", "vpc name")
|
||||
bind_ip := flag.String("ip", "", "bind ip")
|
||||
bind_port := flag.String("port", "", "bind port")
|
||||
ssh_key := flag.String("key", "", "Clef ssh")
|
||||
password := flag.String("pass", "", "password user")
|
||||
start := flag.Bool("start", false, "start metadata server")
|
||||
stop := flag.Bool("stop", false, "stop metadata server")
|
||||
dryrun := flag.Bool("dryrun", false, "launch in dry node")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
conf, err := configuration.LoadConfig(*conf_file)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
db := kv.InitDB(kv.Config{
|
||||
Path: conf.Database.Path,
|
||||
}, false)
|
||||
defer db.Close()
|
||||
|
||||
if *start {
|
||||
if err := metadata.StartMetadata(metadata.NoCloudConfig{
|
||||
VpcName: *vpc,
|
||||
Name: *vm_name,
|
||||
BindIP: *bind_ip,
|
||||
BindPort: *bind_port,
|
||||
Password: *password,
|
||||
SSHKEY: *ssh_key,
|
||||
}, db, *dryrun); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
} else if *stop {
|
||||
if err := metadata.StopMetadata(*vm_name, db, *dryrun); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,26 +2,29 @@ package main
|
|||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
||||
"git.g3e.fr/syonad/two/internal/metadata"
|
||||
)
|
||||
|
||||
var (
|
||||
iface = flag.String("interface", "0.0.0.0", "Interface IP à écouter")
|
||||
port = flag.Int("port", 0, "Port à utiliser")
|
||||
netns_name = flag.String("netns", "", "Network namespace à utiliser")
|
||||
conf_file = flag.String("conf", "/etc/two/agent.yml", "configuration file")
|
||||
confFile = flag.String("conf", "/etc/two/agent.yml", "configuration file")
|
||||
vm_name = flag.String("vm", "", "Name of the vm")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
cfg, err := configuration.LoadConfig(*confFile)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to load config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
metadata.StartServer(metadata.ServerConfig{
|
||||
Netns: *netns_name,
|
||||
Iface: *iface,
|
||||
Port: *port,
|
||||
ConfFile: *conf_file,
|
||||
VmName: *vm_name,
|
||||
RunDir: cfg.Metadata.RunDir,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,16 @@ interfaces:
|
|||
internet: br-000000
|
||||
admin: br-000000
|
||||
|
||||
# Metadata server runtime directory (cloud-init files per VM)
|
||||
metadata:
|
||||
run_dir: "/run/two/metadata"
|
||||
|
||||
# Admin API (read-only DB inspection, loopback only)
|
||||
admin:
|
||||
enabled: false
|
||||
address: "127.0.0.1"
|
||||
port: 9091
|
||||
|
||||
# Logging configuration
|
||||
logger:
|
||||
# Log level: debug, info, warn, error (default: info)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -28,6 +28,14 @@ type Config struct {
|
|||
Level string `mapstructure:"level"`
|
||||
Debug bool `mapstructure:"debug"`
|
||||
} `mapstructure:"logger"`
|
||||
Metadata struct {
|
||||
RunDir string `mapstructure:"run_dir"`
|
||||
} `mapstructure:"metadata"`
|
||||
Admin struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Address string `mapstructure:"address"`
|
||||
Port int `mapstructure:"port"`
|
||||
} `mapstructure:"admin"`
|
||||
DefaultInterface string `mapstructure:"default_interface"`
|
||||
Interfaces map[string]string `mapstructure:"interfaces"`
|
||||
}
|
||||
|
|
@ -46,6 +54,10 @@ func LoadConfig(path string) (*Config, error) {
|
|||
v.SetDefault("worker.buffer_size", 100)
|
||||
v.SetDefault("dispatcher.timeout_seconds", 300)
|
||||
v.SetDefault("dispatcher.poll_seconds", 2)
|
||||
v.SetDefault("metadata.run_dir", "/run/two/metadata")
|
||||
v.SetDefault("admin.enabled", false)
|
||||
v.SetDefault("admin.address", "127.0.0.1")
|
||||
v.SetDefault("admin.port", 9091)
|
||||
v.SetDefault("default_interface", "br-000000")
|
||||
v.SetDefault("logger.level", "info")
|
||||
v.SetDefault("logger.debug", false)
|
||||
|
|
|
|||
19
internal/dhcp/db.go
Normal file
19
internal/dhcp/db.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package dhcp
|
||||
|
||||
import (
|
||||
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
)
|
||||
|
||||
func StoreDHCPEntries(db *badger.DB, subnetName string, entries map[string]string) error {
|
||||
for ip, mac := range entries {
|
||||
if err := kv.AddInDB(db, "subnet/"+subnetName+"/dhcp/"+ip, mac); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetMACForIP(db *badger.DB, subnetName, ip string) (string, error) {
|
||||
return kv.GetFromDB(db, "subnet/"+subnetName+"/dhcp/"+ip)
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ func newConf(t *testing.T, cidr string) Config {
|
|||
|
||||
func TestGenerateConfig_CreatesFile(t *testing.T) {
|
||||
conf := newConf(t, "192.168.1.0/29") // 6 hôtes
|
||||
path, err := GenerateConfig(conf)
|
||||
path, _, err := GenerateConfig(conf)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateConfig a échoué : %v", err)
|
||||
}
|
||||
|
|
@ -73,7 +73,7 @@ func TestGenerateConfig_CreatesFile(t *testing.T) {
|
|||
|
||||
func TestGenerateConfig_FilenameMatchesName(t *testing.T) {
|
||||
conf := newConf(t, "192.168.1.0/29")
|
||||
path, err := GenerateConfig(conf)
|
||||
path, _, err := GenerateConfig(conf)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateConfig a échoué : %v", err)
|
||||
}
|
||||
|
|
@ -86,7 +86,7 @@ func TestGenerateConfig_FilenameMatchesName(t *testing.T) {
|
|||
|
||||
func TestGenerateConfig_ContainsGateway(t *testing.T) {
|
||||
conf := newConf(t, "192.168.1.0/29")
|
||||
path, _ := GenerateConfig(conf)
|
||||
path, _, _ := GenerateConfig(conf)
|
||||
content, _ := os.ReadFile(path)
|
||||
|
||||
if !strings.Contains(string(content), "dhcp-option=3,192.168.1.1") {
|
||||
|
|
@ -102,7 +102,7 @@ func TestGenerateConfig_ContainsDhcpRange(t *testing.T) {
|
|||
Name: "vpc1",
|
||||
ConfDir: t.TempDir(),
|
||||
}
|
||||
path, _ := GenerateConfig(conf)
|
||||
path, _, _ := GenerateConfig(conf)
|
||||
content, _ := os.ReadFile(path)
|
||||
|
||||
if !strings.Contains(string(content), "dhcp-range=10.10.0.0,static,255.255.255.0,12h") {
|
||||
|
|
@ -113,7 +113,7 @@ func TestGenerateConfig_ContainsDhcpRange(t *testing.T) {
|
|||
func TestGenerateConfig_OneHostEntryPerIP(t *testing.T) {
|
||||
// /29 = réseau + broadcast + 6 hôtes → 8 adresses
|
||||
conf := newConf(t, "10.0.0.0/29")
|
||||
path, _ := GenerateConfig(conf)
|
||||
path, _, _ := GenerateConfig(conf)
|
||||
content, _ := os.ReadFile(path)
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
|
|
@ -131,7 +131,7 @@ func TestGenerateConfig_OneHostEntryPerIP(t *testing.T) {
|
|||
|
||||
func TestGenerateConfig_MACPrefix(t *testing.T) {
|
||||
conf := newConf(t, "10.0.0.0/30") // 4 adresses
|
||||
path, _ := GenerateConfig(conf)
|
||||
path, _, _ := GenerateConfig(conf)
|
||||
content, _ := os.ReadFile(path)
|
||||
|
||||
if !strings.Contains(string(content), "00:22:33:") {
|
||||
|
|
@ -148,7 +148,7 @@ func TestGenerateConfig_CreatesConfDir(t *testing.T) {
|
|||
Name: "net",
|
||||
ConfDir: dir,
|
||||
}
|
||||
if _, err := GenerateConfig(conf); err != nil {
|
||||
if _, _, err := GenerateConfig(conf); err != nil {
|
||||
t.Fatalf("GenerateConfig devrait créer les répertoires manquants : %v", err)
|
||||
}
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
func GenerateConfig(c Config) (string, error) {
|
||||
func GenerateConfig(c Config) (string, map[string]string, error) {
|
||||
mask := fmt.Sprintf("%d.%d.%d.%d", c.Network.Mask[0], c.Network.Mask[1], c.Network.Mask[2], c.Network.Mask[3])
|
||||
|
||||
var sb strings.Builder
|
||||
|
|
@ -17,18 +17,20 @@ func GenerateConfig(c Config) (string, error) {
|
|||
fmt.Fprintf(&sb, "dhcp-option=3,%s\n", c.Gateway.String())
|
||||
fmt.Fprintf(&sb, "dhcp-option=6,1.1.1.1,8.8.8.8\n\n")
|
||||
|
||||
entries := make(map[string]string)
|
||||
i := 0
|
||||
for ip := cloneIP(c.Network.IP); c.Network.Contains(ip); incrementIP(ip) {
|
||||
fmt.Fprintf(&sb, "dhcp-host=00:22:33:%02X:%02X:%02X,%s\n",
|
||||
(i>>16)&0xFF, (i>>8)&0xFF, i&0xFF, ip)
|
||||
mac := fmt.Sprintf("00:22:33:%02X:%02X:%02X", (i>>16)&0xFF, (i>>8)&0xFF, i&0xFF)
|
||||
fmt.Fprintf(&sb, "dhcp-host=%s,%s\n", mac, ip)
|
||||
entries[ip.String()] = mac
|
||||
i++
|
||||
}
|
||||
|
||||
outPath := filepath.Join(c.ConfDir, c.Name+".conf")
|
||||
if err := os.MkdirAll(c.ConfDir, 0755); err != nil {
|
||||
return "", err
|
||||
return "", nil, err
|
||||
}
|
||||
return outPath, os.WriteFile(outPath, []byte(sb.String()), 0644)
|
||||
return outPath, entries, os.WriteFile(outPath, []byte(sb.String()), 0644)
|
||||
}
|
||||
|
||||
func incrementIP(ip net.IP) {
|
||||
|
|
|
|||
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, cfg)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
42
internal/iptables/iptables.go
Normal file
42
internal/iptables/iptables.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package iptables
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func addRule(args ...string) error {
|
||||
return exec.Command("iptables", append([]string{"-t", "nat", "-A"}, args...)...).Run()
|
||||
}
|
||||
|
||||
func deleteRule(args ...string) error {
|
||||
return exec.Command("iptables", append([]string{"-t", "nat", "-D"}, args...)...).Run()
|
||||
}
|
||||
|
||||
func AddMetadataRedirect(vmIP, gatewayIP, metadataPort string) error {
|
||||
if err := addRule("PREROUTING",
|
||||
"-s", vmIP+"/32",
|
||||
"-d", "169.254.169.254/32",
|
||||
"-p", "tcp", "-m", "tcp",
|
||||
"--dport", "80",
|
||||
"-j", "DNAT",
|
||||
"--to-destination", gatewayIP+":"+metadataPort,
|
||||
); err != nil {
|
||||
return fmt.Errorf("iptables metadata redirect: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteMetadataRedirect(vmIP, gatewayIP, metadataPort string) error {
|
||||
if err := deleteRule("PREROUTING",
|
||||
"-s", vmIP+"/32",
|
||||
"-d", "169.254.169.254/32",
|
||||
"-p", "tcp", "-m", "tcp",
|
||||
"--dport", "80",
|
||||
"-j", "DNAT",
|
||||
"--to-destination", gatewayIP+":"+metadataPort,
|
||||
); err != nil {
|
||||
return fmt.Errorf("iptables delete metadata redirect: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -3,18 +3,18 @@ package metadata
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
||||
"git.g3e.fr/syonad/two/pkg/systemd"
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
)
|
||||
|
||||
func StartMetadata(config NoCloudConfig, db *badger.DB, dryrun bool) error {
|
||||
func StartMetadata(config NoCloudConfig, cfg *configuration.Config, dryrun bool) error {
|
||||
service, err := systemd.New()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to systemd: %w", err)
|
||||
}
|
||||
defer service.Close()
|
||||
|
||||
LoadNcCloudInDB(config, db)
|
||||
LoadNcCloudInDB(config, cfg.Metadata.RunDir)
|
||||
if !dryrun {
|
||||
if err := service.Start("metadata@" + config.Name + ".service"); err != nil {
|
||||
return fmt.Errorf("failed to start metadata@%s: %w", config.Name, err)
|
||||
|
|
@ -23,17 +23,17 @@ func StartMetadata(config NoCloudConfig, db *badger.DB, dryrun bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func StopMetadata(vm_name string, db *badger.DB, dryrun bool) error {
|
||||
func StopMetadata(vmName string, cfg *configuration.Config, dryrun bool) error {
|
||||
service, err := systemd.New()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to systemd: %w", err)
|
||||
}
|
||||
defer service.Close()
|
||||
|
||||
UnLoadNoCloudInDB(vm_name, db)
|
||||
UnLoadNoCloudInDB(vmName, cfg.Metadata.RunDir)
|
||||
if !dryrun {
|
||||
if err := service.Stop("metadata@" + vm_name + ".service"); err != nil {
|
||||
return fmt.Errorf("failed to stop metadata@%s: %w", vm_name, err)
|
||||
if err := service.Stop("metadata@" + vmName + ".service"); err != nil {
|
||||
return fmt.Errorf("failed to stop metadata@%s: %w", vmName, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ package metadata
|
|||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||
)
|
||||
|
||||
func newCfg() NoCloudConfig {
|
||||
|
|
@ -20,11 +20,9 @@ func newCfg() NoCloudConfig {
|
|||
}
|
||||
}
|
||||
|
||||
func newTestDB(t *testing.T) interface{ Close() error } {
|
||||
func useTestDir(t *testing.T) string {
|
||||
t.Helper()
|
||||
db := kv.InitDB(kv.Config{Path: t.TempDir()}, false)
|
||||
t.Cleanup(func() { db.Close() })
|
||||
return db
|
||||
return t.TempDir()
|
||||
}
|
||||
|
||||
// --- RenderConfig ---
|
||||
|
|
@ -108,78 +106,67 @@ func TestRenderConfig_SpecialCharsInName(t *testing.T) {
|
|||
|
||||
// --- LoadNcCloudInDB / UnLoadNoCloudInDB ---
|
||||
|
||||
func TestLoadNcCloudInDB_StoresAllKeys(t *testing.T) {
|
||||
db := kv.InitDB(kv.Config{Path: t.TempDir()}, false)
|
||||
t.Cleanup(func() { db.Close() })
|
||||
|
||||
cfg := newCfg()
|
||||
LoadNcCloudInDB(cfg, db)
|
||||
|
||||
keys := []string{
|
||||
"metadata/vm1/meta-data",
|
||||
"metadata/vm1/user-data",
|
||||
"metadata/vm1/network-config",
|
||||
"metadata/vm1/vendor-data",
|
||||
"metadata/vm1/vpc",
|
||||
"metadata/vm1/bind_ip",
|
||||
"metadata/vm1/bind_port",
|
||||
}
|
||||
for _, key := range keys {
|
||||
val, err := kv.GetFromDB(db, key)
|
||||
func readTestFile(t *testing.T, dir, vmName, name string) string {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(filepath.Join(dir, vmName, name))
|
||||
if err != nil {
|
||||
t.Errorf("clé %q absente après LoadNcCloudInDB : %v", key, err)
|
||||
t.Errorf("fichier %q absent après LoadNcCloudInDB : %v", name, err)
|
||||
return ""
|
||||
}
|
||||
if val == "" && key != "metadata/vm1/user-data" {
|
||||
t.Errorf("clé %q vide après LoadNcCloudInDB", key)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestLoadNcCloudInDB_StoresAllFiles(t *testing.T) {
|
||||
dir := useTestDir(t)
|
||||
LoadNcCloudInDB(newCfg(), dir)
|
||||
|
||||
files := []string{"meta-data", "user-data", "network-config", "vendor-data", "vpc", "bind_ip", "bind_port"}
|
||||
for _, f := range files {
|
||||
path := filepath.Join(dir, "vm1", f)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Errorf("fichier %q absent : %v", f, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNcCloudInDB_VpcAndBindValues(t *testing.T) {
|
||||
db := kv.InitDB(kv.Config{Path: t.TempDir()}, false)
|
||||
t.Cleanup(func() { db.Close() })
|
||||
dir := useTestDir(t)
|
||||
LoadNcCloudInDB(newCfg(), dir)
|
||||
|
||||
cfg := newCfg()
|
||||
LoadNcCloudInDB(cfg, db)
|
||||
|
||||
vpc, _ := kv.GetFromDB(db, "metadata/vm1/vpc")
|
||||
if vpc != "vpc-test" {
|
||||
if vpc := readTestFile(t, dir, "vm1", "vpc"); vpc != "vpc-test" {
|
||||
t.Errorf("vpc attendu %q, obtenu %q", "vpc-test", vpc)
|
||||
}
|
||||
|
||||
ip, _ := kv.GetFromDB(db, "metadata/vm1/bind_ip")
|
||||
if ip != "169.254.169.254" {
|
||||
if ip := readTestFile(t, dir, "vm1", "bind_ip"); ip != "169.254.169.254" {
|
||||
t.Errorf("bind_ip attendu %q, obtenu %q", "169.254.169.254", ip)
|
||||
}
|
||||
|
||||
port, _ := kv.GetFromDB(db, "metadata/vm1/bind_port")
|
||||
if port != "80" {
|
||||
if port := readTestFile(t, dir, "vm1", "bind_port"); port != "80" {
|
||||
t.Errorf("bind_port attendu %q, obtenu %q", "80", port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnLoadNoCloudInDB_RemovesAllKeys(t *testing.T) {
|
||||
db := kv.InitDB(kv.Config{Path: t.TempDir()}, false)
|
||||
t.Cleanup(func() { db.Close() })
|
||||
func TestUnLoadNoCloudInDB_RemovesAllFiles(t *testing.T) {
|
||||
dir := useTestDir(t)
|
||||
LoadNcCloudInDB(newCfg(), dir)
|
||||
UnLoadNoCloudInDB("vm1", dir)
|
||||
|
||||
cfg := newCfg()
|
||||
LoadNcCloudInDB(cfg, db)
|
||||
UnLoadNoCloudInDB("vm1", db)
|
||||
if _, err := os.Stat(filepath.Join(dir, "vm1")); !os.IsNotExist(err) {
|
||||
t.Error("répertoire vm1 devrait être supprimé après UnLoadNoCloudInDB")
|
||||
}
|
||||
}
|
||||
|
||||
keys := []string{
|
||||
"metadata/vm1/meta-data",
|
||||
"metadata/vm1/user-data",
|
||||
"metadata/vm1/network-config",
|
||||
"metadata/vm1/vendor-data",
|
||||
"metadata/vm1/vpc",
|
||||
"metadata/vm1/bind_ip",
|
||||
"metadata/vm1/bind_port",
|
||||
}
|
||||
for _, key := range keys {
|
||||
_, err := kv.GetFromDB(db, key)
|
||||
if err == nil {
|
||||
t.Errorf("clé %q devrait être supprimée après UnLoadNoCloudInDB", key)
|
||||
}
|
||||
func TestUnLoadNoCloudInDB_DoesNotAffectOtherVMs(t *testing.T) {
|
||||
dir := useTestDir(t)
|
||||
|
||||
cfg1 := newCfg()
|
||||
cfg2 := newCfg()
|
||||
cfg2.Name = "vm2"
|
||||
LoadNcCloudInDB(cfg1, dir)
|
||||
LoadNcCloudInDB(cfg2, dir)
|
||||
|
||||
UnLoadNoCloudInDB("vm1", dir)
|
||||
|
||||
if _, err := os.Stat(filepath.Join(dir, "vm2", "vpc")); err != nil {
|
||||
t.Errorf("vm2 ne devrait pas être supprimée : %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -277,23 +264,3 @@ func TestRootHandler_ContentType(t *testing.T) {
|
|||
t.Errorf("Content-Type attendu text/yaml, obtenu %q", ct)
|
||||
}
|
||||
}
|
||||
|
||||
// --- UnLoadNoCloudInDB_DoesNotAffectOtherVMs ---
|
||||
|
||||
func TestUnLoadNoCloudInDB_DoesNotAffectOtherVMs(t *testing.T) {
|
||||
db := kv.InitDB(kv.Config{Path: t.TempDir()}, false)
|
||||
t.Cleanup(func() { db.Close() })
|
||||
|
||||
cfg1 := newCfg()
|
||||
cfg2 := newCfg()
|
||||
cfg2.Name = "vm2"
|
||||
LoadNcCloudInDB(cfg1, db)
|
||||
LoadNcCloudInDB(cfg2, db)
|
||||
|
||||
UnLoadNoCloudInDB("vm1", db)
|
||||
|
||||
_, err := kv.GetFromDB(db, "metadata/vm2/vpc")
|
||||
if err != nil {
|
||||
t.Errorf("vm2 ne devrait pas être supprimée : %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@ package metadata
|
|||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
)
|
||||
|
||||
//go:embed templates/*.tmpl
|
||||
|
|
@ -26,21 +25,25 @@ func RenderConfig(path string, cfg NoCloudConfig) (string, error) {
|
|||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func LoadNcCloudInDB(config NoCloudConfig, db *badger.DB) {
|
||||
func LoadNcCloudInDB(config NoCloudConfig, runDir string) {
|
||||
meta_data, _ := RenderConfig("templates/meta-data.tmpl", config)
|
||||
user_data, _ := RenderConfig("templates/user-data.tmpl", config)
|
||||
network_config, _ := RenderConfig("templates/network-config.tmpl", config)
|
||||
vendor_data, _ := RenderConfig("templates/vendor-data.tmpl", config)
|
||||
|
||||
kv.AddInDB(db, "metadata/"+config.Name+"/meta-data", meta_data)
|
||||
kv.AddInDB(db, "metadata/"+config.Name+"/user-data", user_data)
|
||||
kv.AddInDB(db, "metadata/"+config.Name+"/network-config", network_config)
|
||||
kv.AddInDB(db, "metadata/"+config.Name+"/vendor-data", vendor_data)
|
||||
kv.AddInDB(db, "metadata/"+config.Name+"/vpc", config.VpcName)
|
||||
kv.AddInDB(db, "metadata/"+config.Name+"/bind_ip", config.BindIP)
|
||||
kv.AddInDB(db, "metadata/"+config.Name+"/bind_port", config.BindPort)
|
||||
dir := filepath.Join(runDir, config.Name)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
os.WriteFile(filepath.Join(dir, "meta-data"), []byte(meta_data), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "user-data"), []byte(user_data), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "network-config"), []byte(network_config), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "vendor-data"), []byte(vendor_data), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "vpc"), []byte(config.VpcName), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "bind_ip"), []byte(config.BindIP), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "bind_port"), []byte(config.BindPort), 0644)
|
||||
}
|
||||
|
||||
func UnLoadNoCloudInDB(vm_name string, db *badger.DB) {
|
||||
kv.DeleteInDB(db, "metadata/"+vm_name)
|
||||
func UnLoadNoCloudInDB(vmName string, runDir string) {
|
||||
os.RemoveAll(filepath.Join(runDir, vmName))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ import (
|
|||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
||||
"git.g3e.fr/syonad/two/internal/netns"
|
||||
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||
)
|
||||
|
||||
var data NoCloudData
|
||||
|
|
@ -23,47 +24,23 @@ func getIP(r *http.Request) string {
|
|||
return ip
|
||||
}
|
||||
|
||||
func getFromDB(config ServerConfig) NoCloudData {
|
||||
var netns_name string
|
||||
var port int
|
||||
var iface string
|
||||
|
||||
conf_db, _ := configuration.LoadConfig(config.ConfFile)
|
||||
|
||||
db := kv.InitDB(kv.Config{Path: conf_db.Database.Path}, true)
|
||||
defer db.Close()
|
||||
|
||||
metadata, _ := kv.GetFromDB(db, "metadata/"+config.VmName+"/meta-data")
|
||||
userdata, _ := kv.GetFromDB(db, "metadata/"+config.VmName+"/user-data")
|
||||
networkconfig, _ := kv.GetFromDB(db, "metadata/"+config.VmName+"/network-config")
|
||||
vendordata, _ := kv.GetFromDB(db, "metadata/"+config.VmName+"/vendor-data")
|
||||
|
||||
if config.Netns == "" {
|
||||
netns_name, _ = kv.GetFromDB(db, "metadata/"+config.VmName+"/vpc")
|
||||
} else {
|
||||
netns_name = config.Netns
|
||||
func readFile(dir, name string) string {
|
||||
b, _ := os.ReadFile(filepath.Join(dir, name))
|
||||
return strings.TrimRight(string(b), "\n")
|
||||
}
|
||||
|
||||
if config.Iface == "" {
|
||||
iface, _ = kv.GetFromDB(db, "metadata/"+config.VmName+"/bind_ip")
|
||||
} else {
|
||||
iface = config.Iface
|
||||
}
|
||||
func getFromFiles(config ServerConfig) NoCloudData {
|
||||
dir := filepath.Join(config.RunDir, config.VmName)
|
||||
|
||||
if config.Port == 0 {
|
||||
sport, _ := kv.GetFromDB(db, "metadata/"+config.VmName+"/bind_port")
|
||||
port, _ = strconv.Atoi(sport)
|
||||
} else {
|
||||
port = config.Port
|
||||
}
|
||||
port, _ := strconv.Atoi(readFile(dir, "bind_port"))
|
||||
|
||||
return NoCloudData{
|
||||
MetaData: metadata,
|
||||
UserData: userdata,
|
||||
NetworkConfig: networkconfig,
|
||||
VendorData: vendordata,
|
||||
NetNs: netns_name,
|
||||
Iface: iface,
|
||||
MetaData: readFile(dir, "meta-data"),
|
||||
UserData: readFile(dir, "user-data"),
|
||||
NetworkConfig: readFile(dir, "network-config"),
|
||||
VendorData: readFile(dir, "vendor-data"),
|
||||
NetNs: readFile(dir, "vpc"),
|
||||
Iface: readFile(dir, "bind_ip"),
|
||||
Port: port,
|
||||
}
|
||||
}
|
||||
|
|
@ -93,7 +70,7 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func StartServer(config ServerConfig) {
|
||||
data = getFromDB(config)
|
||||
data = getFromFiles(config)
|
||||
|
||||
if data.NetNs != "" {
|
||||
if err := netns.Enter(data.NetNs); err != nil {
|
||||
|
|
|
|||
|
|
@ -11,12 +11,8 @@ type NoCloudData struct {
|
|||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Netns string
|
||||
File string
|
||||
Iface string
|
||||
Port int
|
||||
ConfFile string
|
||||
VmName string
|
||||
RunDir string
|
||||
}
|
||||
|
||||
type NoCloudConfig struct {
|
||||
|
|
|
|||
34
internal/netif/tap_linux.go
Normal file
34
internal/netif/tap_linux.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
//go:build linux
|
||||
|
||||
package netif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.g3e.fr/syonad/two/internal/netns"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
func CreateTap(tapID int, bridgeName, vpcName string) error {
|
||||
name := fmt.Sprintf("tap%d", tapID)
|
||||
|
||||
return netns.Call(vpcName, func() error {
|
||||
tap := &netlink.Tuntap{
|
||||
LinkAttrs: netlink.LinkAttrs{Name: name},
|
||||
Mode: netlink.TUNTAP_MODE_TAP,
|
||||
}
|
||||
if err := netlink.LinkAdd(tap); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := BridgeSetMaster(name, bridgeName); err != nil {
|
||||
return err
|
||||
}
|
||||
return LinkSetUp(name)
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteTap(tapID int, vpcName string) error {
|
||||
return netns.Call(vpcName, func() error {
|
||||
return DeleteLink(fmt.Sprintf("tap%d", tapID))
|
||||
})
|
||||
}
|
||||
13
internal/netif/tap_other.go
Normal file
13
internal/netif/tap_other.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
//go:build !linux
|
||||
|
||||
package netif
|
||||
|
||||
import "errors"
|
||||
|
||||
func CreateTap(_ int, _, _ string) error {
|
||||
return errors.New("netif: tap not supported on this platform")
|
||||
}
|
||||
|
||||
func DeleteTap(_ int, _ string) error {
|
||||
return errors.New("netif: tap not supported on this platform")
|
||||
}
|
||||
48
internal/qemu/start_linux.go
Normal file
48
internal/qemu/start_linux.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
//go:build linux
|
||||
|
||||
package qemu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Name string
|
||||
TapID int
|
||||
Mac string
|
||||
VolumePath string
|
||||
Memory int
|
||||
CPUs int
|
||||
}
|
||||
|
||||
func Start(cfg Config) error {
|
||||
memory := cfg.Memory
|
||||
if memory == 0 {
|
||||
memory = 512
|
||||
}
|
||||
|
||||
cpus := cfg.CPUs
|
||||
if cpus == 0 {
|
||||
cpus = 1
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
14
internal/qemu/start_other.go
Normal file
14
internal/qemu/start_other.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
//go:build !linux
|
||||
|
||||
package qemu
|
||||
|
||||
import "errors"
|
||||
|
||||
type Config struct {
|
||||
Name, Mac, VolumePath string
|
||||
TapID, Memory, CPUs int
|
||||
}
|
||||
|
||||
func Start(_ Config) error {
|
||||
return errors.New("vm: not supported on this platform")
|
||||
}
|
||||
50
internal/qmp/send.go
Normal file
50
internal/qmp/send.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package qmp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Send(socketPath string, commands []string) ([]json.RawMessage, error) {
|
||||
conn, err := net.Dial("unix", socketPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("qmp dial: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
r := bufio.NewReader(conn)
|
||||
|
||||
if _, err := r.ReadString('\n'); err != nil {
|
||||
return nil, fmt.Errorf("qmp read greeting: %w", err)
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`{ "execute": "qmp_capabilities" }`)
|
||||
for _, cmd := range commands {
|
||||
sb.WriteByte('\n')
|
||||
sb.WriteString(cmd)
|
||||
}
|
||||
sb.WriteByte('\n')
|
||||
|
||||
if _, err := fmt.Fprint(conn, sb.String()); err != nil {
|
||||
return nil, fmt.Errorf("qmp write: %w", err)
|
||||
}
|
||||
|
||||
if _, err := r.ReadString('\n'); err != nil {
|
||||
return nil, fmt.Errorf("qmp read capabilities response: %w", err)
|
||||
}
|
||||
|
||||
var results []json.RawMessage
|
||||
for range commands {
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("qmp read response: %w", err)
|
||||
}
|
||||
results = append(results, json.RawMessage(line))
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
|
@ -2,9 +2,6 @@ package subnet
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.g3e.fr/syonad/two/internal/dhcp"
|
||||
"git.g3e.fr/syonad/two/internal/ebtables"
|
||||
|
|
@ -25,94 +22,51 @@ func CreateSubnet(db *badger.DB, subnetName string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// lecture des paramètres depuis la DB
|
||||
vpcName, err := kv.GetFromDB(db, "subnet/"+subnetName+"/vpc")
|
||||
d, err := loadSubnet(db, subnetName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get vpc: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
vxlanIDStr, err := kv.GetFromDB(db, "subnet/"+subnetName+"/vxlan_id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get vxlan_id: %w", err)
|
||||
}
|
||||
vxlanID, err := strconv.Atoi(vxlanIDStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse vxlan_id: %w", err)
|
||||
}
|
||||
vxlanIface := fmt.Sprintf("vxlan-%d", d.vxlanID)
|
||||
|
||||
localIface, err := kv.GetFromDB(db, "subnet/"+subnetName+"/local_iface")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get local_iface: %w", err)
|
||||
}
|
||||
|
||||
gatewayIPStr, err := kv.GetFromDB(db, "subnet/"+subnetName+"/gateway_ip")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get gateway_ip: %w", err)
|
||||
}
|
||||
gatewayIP := net.ParseIP(gatewayIPStr)
|
||||
if gatewayIP == nil {
|
||||
return fmt.Errorf("invalid gateway_ip: %s", gatewayIPStr)
|
||||
}
|
||||
|
||||
cidr, err := kv.GetFromDB(db, "subnet/"+subnetName+"/cidr")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get cidr: %w", err)
|
||||
}
|
||||
_, subnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse cidr: %w", err)
|
||||
}
|
||||
|
||||
// subnet_id = partie après le premier '-' (ex: "sn-00001" -> "00001")
|
||||
subnetID := strings.SplitN(subnetName, "-", 2)[1]
|
||||
bridge := "br-" + subnetID
|
||||
vxlanIface := fmt.Sprintf("vxlan-%d", vxlanID)
|
||||
|
||||
// veth pair
|
||||
if err := netif.CreateVethToNetns("v-"+subnetID+"-e", "v-"+subnetID+"-i", "/var/run/netns/"+vpcName, 1500); err != nil {
|
||||
if err := netif.CreateVethToNetns("v-"+d.subnetID+"-e", "v-"+d.subnetID+"-i", "/var/run/netns/"+d.vpc, 1500); err != nil {
|
||||
return fmt.Errorf("create veth: %w", err)
|
||||
}
|
||||
|
||||
// bridge dans le root netns
|
||||
if err := netif.CreateBridge(bridge, 1500); err != nil {
|
||||
if err := netif.CreateBridge(d.bridge, 1500); err != nil {
|
||||
return fmt.Errorf("create bridge: %w", err)
|
||||
}
|
||||
|
||||
// bridge dans le netns VPC
|
||||
if err := netns.Call(vpcName, func() error {
|
||||
return netif.CreateBridge(bridge, 1500)
|
||||
if err := netns.Call(d.vpc, func() error {
|
||||
return netif.CreateBridge(d.bridge, 1500)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("create bridge in netns: %w", err)
|
||||
}
|
||||
|
||||
// vxlan
|
||||
if err := netif.CreateVxlan(vxlanIface, vxlanID, localIface, 1500); err != nil {
|
||||
if err := netif.CreateVxlan(vxlanIface, d.vxlanID, d.localIface, 1500); err != nil {
|
||||
return fmt.Errorf("create vxlan: %w", err)
|
||||
}
|
||||
|
||||
// ajout des interfaces dans les bridges
|
||||
if err := netif.BridgeSetMaster("v-"+subnetID+"-e", bridge); err != nil {
|
||||
if err := netif.BridgeSetMaster("v-"+d.subnetID+"-e", d.bridge); err != nil {
|
||||
return fmt.Errorf("add veth-e to bridge: %w", err)
|
||||
}
|
||||
if err := netns.Call(vpcName, func() error {
|
||||
return netif.BridgeSetMaster("v-"+subnetID+"-i", bridge)
|
||||
if err := netns.Call(d.vpc, func() error {
|
||||
return netif.BridgeSetMaster("v-"+d.subnetID+"-i", d.bridge)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("add veth-i to bridge in netns: %w", err)
|
||||
}
|
||||
if err := netif.BridgeSetMaster(vxlanIface, bridge); err != nil {
|
||||
if err := netif.BridgeSetMaster(vxlanIface, d.bridge); err != nil {
|
||||
return fmt.Errorf("add vxlan to bridge: %w", err)
|
||||
}
|
||||
|
||||
// montée des interfaces dans le root netns
|
||||
for _, iface := range []string{"v-" + subnetID + "-e", vxlanIface, bridge} {
|
||||
for _, iface := range []string{"v-" + d.subnetID + "-e", vxlanIface, d.bridge} {
|
||||
if err := netif.LinkSetUp(iface); err != nil {
|
||||
return fmt.Errorf("set up %s: %w", iface, err)
|
||||
}
|
||||
}
|
||||
|
||||
// montée des interfaces dans le netns VPC
|
||||
if err := netns.Call(vpcName, func() error {
|
||||
for _, iface := range []string{"v-" + subnetID + "-i", bridge} {
|
||||
if err := netns.Call(d.vpc, func() error {
|
||||
for _, iface := range []string{"v-" + d.subnetID + "-i", d.bridge} {
|
||||
if err := netif.LinkSetUp(iface); err != nil {
|
||||
return fmt.Errorf("set up %s: %w", iface, err)
|
||||
}
|
||||
|
|
@ -122,37 +76,38 @@ func CreateSubnet(db *badger.DB, subnetName string) error {
|
|||
return fmt.Errorf("set up interfaces in netns: %w", err)
|
||||
}
|
||||
|
||||
// IP gateway (/32) sur le bridge interne
|
||||
if err := netns.Call(vpcName, func() error {
|
||||
return netif.AddrAdd(bridge, gatewayIP)
|
||||
if err := netns.Call(d.vpc, func() error {
|
||||
return netif.AddrAdd(d.bridge, d.gatewayIP)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("add addr to bridge in netns: %w", err)
|
||||
}
|
||||
|
||||
// route subnet (scope link) dans le netns VPC
|
||||
if err := netns.Call(vpcName, func() error {
|
||||
return netif.RouteAdd(bridge, subnet)
|
||||
if err := netns.Call(d.vpc, func() error {
|
||||
return netif.RouteAdd(d.bridge, d.cidr)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("add route in netns: %w", err)
|
||||
}
|
||||
|
||||
if err := ebtables.DropARPToGateway(bridge, gatewayIP.String()); err != nil {
|
||||
if err := ebtables.DropARPToGateway(d.bridge, d.gatewayIP.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ebtables.DropDHCP(bridge); err != nil {
|
||||
if err := ebtables.DropDHCP(d.bridge); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// génération de la config dnsmasq et démarrage du service
|
||||
conf := dhcp.Config{
|
||||
Network: subnet,
|
||||
Gateway: gatewayIP,
|
||||
Name: vpcName + "_" + bridge,
|
||||
Network: d.cidr,
|
||||
Gateway: d.gatewayIP,
|
||||
Name: d.vpc + "_" + d.bridge,
|
||||
ConfDir: "/etc/dnsmasq.d",
|
||||
}
|
||||
if _, err := dhcp.GenerateConfig(conf); err != nil {
|
||||
_, entries, err := dhcp.GenerateConfig(conf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate dhcp config: %w", err)
|
||||
}
|
||||
if err := dhcp.StoreDHCPEntries(db, subnetName, entries); err != nil {
|
||||
return fmt.Errorf("store dhcp entries: %w", err)
|
||||
}
|
||||
|
||||
svc, err := systemd.New()
|
||||
if err != nil {
|
||||
|
|
|
|||
72
internal/subnet/data.go
Normal file
72
internal/subnet/data.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package subnet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
)
|
||||
|
||||
type subnetData struct {
|
||||
vpc string
|
||||
subnetID string
|
||||
bridge string
|
||||
vxlanID int
|
||||
localIface string
|
||||
gatewayIP net.IP
|
||||
cidr *net.IPNet
|
||||
}
|
||||
|
||||
func loadSubnet(db *badger.DB, name string) (subnetData, error) {
|
||||
var d subnetData
|
||||
|
||||
d.subnetID = strings.SplitN(name, "-", 2)[1]
|
||||
d.bridge = "br-" + d.subnetID
|
||||
|
||||
vpc, err := kv.GetFromDB(db, "subnet/"+name+"/vpc")
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("get vpc: %w", err)
|
||||
}
|
||||
d.vpc = vpc
|
||||
|
||||
vxlanIDStr, err := kv.GetFromDB(db, "subnet/"+name+"/vxlan_id")
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("get vxlan_id: %w", err)
|
||||
}
|
||||
vxlanID, err := strconv.Atoi(vxlanIDStr)
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("parse vxlan_id: %w", err)
|
||||
}
|
||||
d.vxlanID = vxlanID
|
||||
|
||||
localIface, err := kv.GetFromDB(db, "subnet/"+name+"/local_iface")
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("get local_iface: %w", err)
|
||||
}
|
||||
d.localIface = localIface
|
||||
|
||||
gatewayIPStr, err := kv.GetFromDB(db, "subnet/"+name+"/gateway_ip")
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("get gateway_ip: %w", err)
|
||||
}
|
||||
gatewayIP := net.ParseIP(gatewayIPStr)
|
||||
if gatewayIP == nil {
|
||||
return d, fmt.Errorf("invalid gateway_ip: %s", gatewayIPStr)
|
||||
}
|
||||
d.gatewayIP = gatewayIP
|
||||
|
||||
cidrStr, err := kv.GetFromDB(db, "subnet/"+name+"/cidr")
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("get cidr: %w", err)
|
||||
}
|
||||
_, ipNet, err := net.ParseCIDR(cidrStr)
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("parse cidr: %w", err)
|
||||
}
|
||||
d.cidr = ipNet
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package subnet
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.g3e.fr/syonad/two/internal/ebtables"
|
||||
"git.g3e.fr/syonad/two/internal/netif"
|
||||
|
|
@ -23,69 +22,52 @@ func DeleteSubnet(db *badger.DB, subnetName string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
vpcName, err := kv.GetFromDB(db, "subnet/"+subnetName+"/vpc")
|
||||
d, err := loadSubnet(db, subnetName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get vpc: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
vxlanIDStr, err := kv.GetFromDB(db, "subnet/"+subnetName+"/vxlan_id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get vxlan_id: %w", err)
|
||||
}
|
||||
vxlanIface := fmt.Sprintf("vxlan-%d", d.vxlanID)
|
||||
|
||||
gatewayIP, err := kv.GetFromDB(db, "subnet/"+subnetName+"/gateway_ip")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get gateway_ip: %w", err)
|
||||
}
|
||||
|
||||
subnetID := strings.SplitN(subnetName, "-", 2)[1]
|
||||
bridge := "br-" + subnetID
|
||||
vxlanIface := "vxlan-" + vxlanIDStr
|
||||
|
||||
// arrêt du service dnsmasq
|
||||
svc, err := systemd.New()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to systemd: %w", err)
|
||||
}
|
||||
defer svc.Close()
|
||||
|
||||
svcName := "dnsmasq@" + vpcName + "_" + bridge + ".service"
|
||||
if err := svc.Stop(svcName); err != nil {
|
||||
if err := svc.Stop("dnsmasq@" + d.vpc + "_" + d.bridge + ".service"); err != nil {
|
||||
return fmt.Errorf("stop dnsmasq: %w", err)
|
||||
}
|
||||
|
||||
// suppression de la config dnsmasq
|
||||
if err := os.Remove("/etc/dnsmasq.d/" + vpcName + "_" + bridge + ".conf"); err != nil && !os.IsNotExist(err) {
|
||||
if err := os.Remove("/etc/dnsmasq.d/" + d.vpc + "_" + d.bridge + ".conf"); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("remove dnsmasq config: %w", err)
|
||||
}
|
||||
if err := kv.DeleteInDB(db, "subnet/"+subnetName+"/dhcp"); err != nil {
|
||||
return fmt.Errorf("delete dhcp entries: %w", err)
|
||||
}
|
||||
|
||||
// suppression des règles ebtables
|
||||
if err := ebtables.DeleteARPToGateway(bridge, gatewayIP); err != nil {
|
||||
if err := ebtables.DeleteARPToGateway(d.bridge, d.gatewayIP.String()); err != nil {
|
||||
return fmt.Errorf("delete ebtables arp rule: %w", err)
|
||||
}
|
||||
if err := ebtables.DeleteDHCP(bridge); err != nil {
|
||||
if err := ebtables.DeleteDHCP(d.bridge); err != nil {
|
||||
return fmt.Errorf("delete ebtables dhcp rule: %w", err)
|
||||
}
|
||||
|
||||
// suppression du bridge dans le netns VPC
|
||||
if err := netns.Call(vpcName, func() error {
|
||||
return netif.DeleteLink(bridge)
|
||||
if err := netns.Call(d.vpc, func() error {
|
||||
return netif.DeleteLink(d.bridge)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("delete bridge in netns: %w", err)
|
||||
}
|
||||
|
||||
// suppression du vxlan
|
||||
if err := netif.DeleteLink(vxlanIface); err != nil {
|
||||
return fmt.Errorf("delete vxlan: %w", err)
|
||||
}
|
||||
|
||||
// suppression du veth pair (supprime les deux côtés)
|
||||
if err := netif.DeleteLink("v-" + subnetID + "-e"); err != nil {
|
||||
if err := netif.DeleteLink("v-" + d.subnetID + "-e"); err != nil {
|
||||
return fmt.Errorf("delete veth: %w", err)
|
||||
}
|
||||
|
||||
// suppression du bridge dans le root netns
|
||||
if err := netif.DeleteLink(bridge); err != nil {
|
||||
if err := netif.DeleteLink(d.bridge); err != nil {
|
||||
return fmt.Errorf("delete bridge: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
66
internal/vm/create.go
Normal file
66
internal/vm/create.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package vm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
||||
"git.g3e.fr/syonad/two/internal/iptables"
|
||||
"git.g3e.fr/syonad/two/internal/metadata"
|
||||
"git.g3e.fr/syonad/two/internal/netif"
|
||||
"git.g3e.fr/syonad/two/internal/netns"
|
||||
"git.g3e.fr/syonad/two/internal/qemu"
|
||||
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
)
|
||||
|
||||
func StartVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
||||
state, err := kv.GetFromDB(db, "vm/"+name+"/state")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if state != "starting" {
|
||||
return nil
|
||||
}
|
||||
|
||||
d, err := loadVM(db, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := netif.CreateTap(d.tapID, d.bridge, d.vpcName); err != nil {
|
||||
return fmt.Errorf("create tap: %w", err)
|
||||
}
|
||||
|
||||
if err := netns.Call(d.vpcName, func() error {
|
||||
return iptables.AddMetadataRedirect(d.ip, d.gatewayIP, d.metadataPort)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("add metadata redirect: %w", err)
|
||||
}
|
||||
|
||||
if err := metadata.StartMetadata(metadata.NoCloudConfig{
|
||||
Name: name,
|
||||
VpcName: d.vpcName,
|
||||
BindIP: d.gatewayIP,
|
||||
BindPort: d.metadataPort,
|
||||
Password: d.password,
|
||||
SSHKEY: d.sshkey,
|
||||
}, cfg, false); err != nil {
|
||||
return fmt.Errorf("start metadata: %w", err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return kv.AddInDB(db, "vm/"+name+"/state", "started")
|
||||
}
|
||||
112
internal/vm/data.go
Normal file
112
internal/vm/data.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
package vm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.g3e.fr/syonad/two/internal/dhcp"
|
||||
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
)
|
||||
|
||||
type vmData struct {
|
||||
subnetName string
|
||||
vpcName string
|
||||
gatewayIP string
|
||||
bridge string
|
||||
tapID int
|
||||
ip string
|
||||
metadataPort string
|
||||
mac string
|
||||
volumePath string
|
||||
memory int
|
||||
cpus int
|
||||
password string
|
||||
sshkey string
|
||||
}
|
||||
|
||||
func loadVM(db *badger.DB, name string) (vmData, error) {
|
||||
var d vmData
|
||||
|
||||
subnetName, err := kv.GetFromDB(db, "vm/"+name+"/subnet")
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("get subnet: %w", err)
|
||||
}
|
||||
d.subnetName = subnetName
|
||||
d.bridge = "br-" + strings.SplitN(subnetName, "-", 2)[1]
|
||||
|
||||
vpcName, err := kv.GetFromDB(db, "subnet/"+subnetName+"/vpc")
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("get vpc: %w", err)
|
||||
}
|
||||
d.vpcName = vpcName
|
||||
|
||||
gatewayIP, err := kv.GetFromDB(db, "subnet/"+subnetName+"/gateway_ip")
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("get gateway_ip: %w", err)
|
||||
}
|
||||
d.gatewayIP = gatewayIP
|
||||
|
||||
tapIDStr, err := kv.GetFromDB(db, "vm/"+name+"/tap_id")
|
||||
if err != nil {
|
||||
d.tapID = rand.Intn(90000000) + 10000000
|
||||
if err := kv.AddInDB(db, "vm/"+name+"/tap_id", strconv.Itoa(d.tapID)); err != nil {
|
||||
return d, fmt.Errorf("store tap_id: %w", err)
|
||||
}
|
||||
} else {
|
||||
tapID, err := strconv.Atoi(tapIDStr)
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("parse tap_id: %w", err)
|
||||
}
|
||||
d.tapID = tapID
|
||||
}
|
||||
|
||||
ip, err := kv.GetFromDB(db, "vm/"+name+"/ip")
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("get ip: %w", err)
|
||||
}
|
||||
d.ip = ip
|
||||
|
||||
metadataPort, err := kv.GetFromDB(db, "vm/"+name+"/metadata_port")
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("get metadata_port: %w", err)
|
||||
}
|
||||
d.metadataPort = metadataPort
|
||||
|
||||
mac, err := dhcp.GetMACForIP(db, d.subnetName, d.ip)
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("get mac for ip %s: %w", d.ip, err)
|
||||
}
|
||||
d.mac = mac
|
||||
|
||||
volumePath, err := kv.GetFromDB(db, "vm/"+name+"/volume_path")
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("get volume_path: %w", err)
|
||||
}
|
||||
d.volumePath = volumePath
|
||||
|
||||
memoryStr, err := kv.GetFromDB(db, "vm/"+name+"/memory")
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("get memory: %w", err)
|
||||
}
|
||||
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, 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")
|
||||
|
||||
return d, nil
|
||||
}
|
||||
69
internal/vm/delete.go
Normal file
69
internal/vm/delete.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package vm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
||||
"git.g3e.fr/syonad/two/internal/iptables"
|
||||
"git.g3e.fr/syonad/two/internal/metadata"
|
||||
"git.g3e.fr/syonad/two/internal/netif"
|
||||
"git.g3e.fr/syonad/two/internal/netns"
|
||||
"git.g3e.fr/syonad/two/internal/qmp"
|
||||
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
)
|
||||
|
||||
func StopVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
||||
state, err := kv.GetFromDB(db, "vm/"+name+"/state")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if state != "stopping" {
|
||||
return nil
|
||||
}
|
||||
|
||||
d, err := loadVM(db, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
socketPath := fmt.Sprintf("/tmp/%s.qmp-sock", name)
|
||||
|
||||
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 := netns.Call(d.vpcName, func() error {
|
||||
return iptables.DeleteMetadataRedirect(d.ip, d.gatewayIP, d.metadataPort)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("delete metadata redirect: %w", err)
|
||||
}
|
||||
|
||||
if err := metadata.StopMetadata(name, cfg, false); err != nil {
|
||||
return fmt.Errorf("stop metadata: %w", err)
|
||||
}
|
||||
|
||||
if err := netif.DeleteTap(d.tapID, d.vpcName); err != nil {
|
||||
return fmt.Errorf("delete tap: %w", err)
|
||||
}
|
||||
|
||||
return kv.AddInDB(db, "vm/"+name+"/state", "stopped")
|
||||
}
|
||||
52
pkg/db/kv/admin_server.go
Normal file
52
pkg/db/kv/admin_server.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package kv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
)
|
||||
|
||||
type AdminServer struct {
|
||||
db *badger.DB
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewAdminServer(db *badger.DB, logger *slog.Logger) *AdminServer {
|
||||
return &AdminServer{db: db, logger: logger}
|
||||
}
|
||||
|
||||
func (s *AdminServer) Start(address string) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/db", s.dbHandler)
|
||||
s.logger.Info("admin server listening", "address", address)
|
||||
if err := http.ListenAndServe(address, mux); err != nil {
|
||||
s.logger.Error("admin server stopped", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AdminServer) dbHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
entries, err := ListByPrefix(s.db, r.URL.Query().Get("prefix"))
|
||||
if err != nil {
|
||||
http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(entries))
|
||||
for k := range entries {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
for _, k := range keys {
|
||||
fmt.Fprintf(w, "%s=%s\n", k, entries[k])
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package kv
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
)
|
||||
|
||||
|
|
@ -15,6 +17,7 @@ func InitDB(conf Config, readonly bool) *badger.DB {
|
|||
opts.NumLevelZeroTablesStall = 2
|
||||
db, err := badger.Open(opts)
|
||||
if err != nil {
|
||||
log.Printf("kv.InitDB (readonly=%v, path=%s): %v", readonly, conf.Path, err)
|
||||
panic(err)
|
||||
}
|
||||
return db
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue