All checks were successful
Release Pipeline / checksums (push) Successful in 4s
Release Pipeline / release (push) Successful in 11s
Release Pipeline / set-release-target (push) Successful in 2s
Release Pipeline / upload-assets (agent.service, systemd/agent.service) (push) Successful in 4s
Release Pipeline / upload-assets (dnsmasq@.service, systemd/dnsmasq@.service) (push) Successful in 4s
Release Pipeline / upload-assets (metadata@.service, systemd/metadata@.service) (push) Successful in 4s
Release Pipeline / upload-assets (run-dnsmasq-in-netns.sh, scripts/run-dnsmasq-in-netns.sh) (push) Successful in 4s
Release Pipeline / build (agent, amd64, linux) (push) Successful in 0s
Release Pipeline / build (metadata, amd64, linux) (push) Successful in 0s
Release Pipeline / publish (push) Successful in 0s
Release Pipeline / build (push) Successful in 1m36s
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
238 lines
5.9 KiB
Go
238 lines
5.9 KiB
Go
package vm
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"net"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.g3e.fr/syonad/two/internal/dhcp"
|
|
"git.g3e.fr/syonad/two/pkg/db/kv"
|
|
"github.com/dgraph-io/badger/v4"
|
|
)
|
|
|
|
type diskEntry struct {
|
|
path string
|
|
dev string
|
|
}
|
|
|
|
type nicData struct {
|
|
index int
|
|
subnetName string
|
|
vpcName string
|
|
bridge string
|
|
interfaceIP string
|
|
ip string
|
|
mac string
|
|
tapID int
|
|
primary bool
|
|
mode string
|
|
vpcCIDR *net.IPNet
|
|
}
|
|
|
|
type vmData struct {
|
|
nics []nicData
|
|
metadataPort string
|
|
disks []diskEntry
|
|
memory int
|
|
cpus int
|
|
uefi bool
|
|
password string
|
|
sshkey string
|
|
documents map[string]string
|
|
}
|
|
|
|
// primary retourne l'interface portant la route par défaut et le serveur de
|
|
// métadonnées. loadVM garantit qu'il y en a exactement une.
|
|
func (d vmData) primary() nicData {
|
|
for _, n := range d.nics {
|
|
if n.primary {
|
|
return n
|
|
}
|
|
}
|
|
return nicData{}
|
|
}
|
|
|
|
func loadVM(db *badger.DB, name string) (vmData, error) {
|
|
var d vmData
|
|
|
|
nics, err := loadNICs(db, name)
|
|
if err != nil {
|
|
return d, err
|
|
}
|
|
d.nics = nics
|
|
|
|
metadataPort, err := kv.GetFromDB(db, "vm/"+name+"/metadata_port")
|
|
if err != nil {
|
|
return d, fmt.Errorf("get metadata_port: %w", err)
|
|
}
|
|
d.metadataPort = metadataPort
|
|
|
|
diskEntries, err := kv.ListByPrefix(db, "vm/"+name+"/disk/")
|
|
if err != nil {
|
|
return d, fmt.Errorf("list disks: %w", err)
|
|
}
|
|
if len(diskEntries) == 0 {
|
|
return d, fmt.Errorf("no disks found for vm %q", name)
|
|
}
|
|
diskPrefix := "vm/" + name + "/disk/"
|
|
for key, path := range diskEntries {
|
|
dev := strings.TrimPrefix(key, diskPrefix)
|
|
d.disks = append(d.disks, diskEntry{path: path, dev: dev})
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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")
|
|
|
|
docPrefix := "vm/" + name + "/metadata/"
|
|
docEntries, err := kv.ListByPrefix(db, docPrefix)
|
|
if err != nil {
|
|
return d, fmt.Errorf("list metadata documents: %w", err)
|
|
}
|
|
if len(docEntries) > 0 {
|
|
d.documents = make(map[string]string, len(docEntries))
|
|
for key, content := range docEntries {
|
|
d.documents[strings.TrimPrefix(key, docPrefix)] = content
|
|
}
|
|
}
|
|
|
|
return d, nil
|
|
}
|
|
|
|
// loadNICs lit les interfaces d'une VM sous vm/<name>/nic/<index>/.
|
|
// Le tap est alloué à la première lecture et persisté, comme avant le passage
|
|
// au multi-interfaces — mais désormais par interface.
|
|
func loadNICs(db *badger.DB, name string) ([]nicData, error) {
|
|
prefix := "vm/" + name + "/nic/"
|
|
entries, err := kv.ListByPrefix(db, prefix)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list nics: %w", err)
|
|
}
|
|
|
|
indexes := make(map[int]bool)
|
|
for key := range entries {
|
|
parts := strings.Split(strings.TrimPrefix(key, prefix), "/")
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
idx, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid nic index %q for vm %s", parts[0], name)
|
|
}
|
|
indexes[idx] = true
|
|
}
|
|
if len(indexes) == 0 {
|
|
return nil, fmt.Errorf("no interface found for vm %q", name)
|
|
}
|
|
|
|
nics := make([]nicData, 0, len(indexes))
|
|
for idx := range indexes {
|
|
n, err := loadNIC(db, name, idx, entries)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nics = append(nics, n)
|
|
}
|
|
sort.Slice(nics, func(i, j int) bool { return nics[i].index < nics[j].index })
|
|
|
|
primaries := 0
|
|
for _, n := range nics {
|
|
if n.primary {
|
|
primaries++
|
|
}
|
|
}
|
|
if primaries != 1 {
|
|
return nil, fmt.Errorf("vm %q has %d primary interfaces, expected exactly one", name, primaries)
|
|
}
|
|
return nics, nil
|
|
}
|
|
|
|
func loadNIC(db *badger.DB, name string, idx int, entries map[string]string) (nicData, error) {
|
|
n := nicData{index: idx}
|
|
prefix := fmt.Sprintf("vm/%s/nic/%d/", name, idx)
|
|
|
|
n.subnetName = entries[prefix+"subnet"]
|
|
if n.subnetName == "" {
|
|
return n, fmt.Errorf("nic %d of vm %s has no subnet", idx, name)
|
|
}
|
|
n.bridge = "br-" + strings.SplitN(n.subnetName, "-", 2)[1]
|
|
n.primary = entries[prefix+"primary"] == "true"
|
|
|
|
vpcName, err := kv.GetFromDB(db, "subnet/"+n.subnetName+"/vpc")
|
|
if err != nil {
|
|
return n, fmt.Errorf("get vpc of subnet %s: %w", n.subnetName, err)
|
|
}
|
|
n.vpcName = vpcName
|
|
|
|
interfaceIP, err := kv.GetFromDB(db, "subnet/"+n.subnetName+"/interface_ip")
|
|
if err != nil {
|
|
return n, fmt.Errorf("get interface_ip of subnet %s: %w", n.subnetName, err)
|
|
}
|
|
n.interfaceIP = interfaceIP
|
|
|
|
n.mode, err = kv.GetFromDB(db, "subnet/"+n.subnetName+"/mode")
|
|
if err != nil {
|
|
return n, fmt.Errorf("get mode of subnet %s: %w", n.subnetName, err)
|
|
}
|
|
|
|
if n.mode != "bridge" {
|
|
cidrStr, err := kv.GetFromDB(db, "vpc/"+n.vpcName+"/cidr")
|
|
if err != nil {
|
|
return n, fmt.Errorf("get cidr of vpc %s: %w", n.vpcName, err)
|
|
}
|
|
_, vpcCIDR, err := net.ParseCIDR(cidrStr)
|
|
if err != nil {
|
|
return n, fmt.Errorf("parse cidr of vpc %s: %w", n.vpcName, err)
|
|
}
|
|
n.vpcCIDR = vpcCIDR
|
|
}
|
|
|
|
n.ip = entries[prefix+"ip"]
|
|
if n.ip == "" {
|
|
return n, fmt.Errorf("nic %d of vm %s has no ip", idx, name)
|
|
}
|
|
|
|
mac, err := dhcp.GetMACForIP(db, n.subnetName, n.ip)
|
|
if err != nil {
|
|
return n, fmt.Errorf("get mac for ip %s: %w", n.ip, err)
|
|
}
|
|
n.mac = mac
|
|
|
|
if tapIDStr, ok := entries[prefix+"tap_id"]; ok {
|
|
tapID, err := strconv.Atoi(tapIDStr)
|
|
if err != nil {
|
|
return n, fmt.Errorf("parse tap_id of nic %d: %w", idx, err)
|
|
}
|
|
n.tapID = tapID
|
|
return n, nil
|
|
}
|
|
|
|
n.tapID = rand.Intn(90000000) + 10000000
|
|
if err := kv.AddInDB(db, prefix+"tap_id", strconv.Itoa(n.tapID)); err != nil {
|
|
return n, fmt.Errorf("store tap_id of nic %d: %w", idx, err)
|
|
}
|
|
return n, nil
|
|
}
|