two/internal/vm/create.go
GnomeZworc 26d5805747
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
f-33: vm: start vms with multiple nic
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-08-25 23:12:55 +02:00

179 lines
4.8 KiB
Go

package vm
import (
"fmt"
"io"
"net"
"os"
"path/filepath"
configuration "git.g3e.fr/syonad/two/internal/config/agent"
"git.g3e.fr/syonad/two/internal/dhcp"
"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/internal/state"
"github.com/dgraph-io/badger/v4"
)
func StartVM(db *badger.DB, name string, cfg *configuration.Config) error {
current, err := state.Get(db, "vm/"+name)
if err != nil {
return err
}
if current != state.Creating {
return nil
}
d, err := loadVM(db, name)
if err != nil {
return err
}
nic := d.primary()
for _, n := range d.nics {
if err := netif.CreateTap(n.tapID, n.bridge, n.vpcName); err != nil {
return fmt.Errorf("create tap of interface %d: %w", n.index, err)
}
}
// La redirection est posée pour chaque IP de la VM vers le serveur de
// métadonnées de l'interface primaire. Toutes les interfaces étant dans le
// même VPC, donc le même netns, il est joignable depuis n'importe laquelle.
if err := netns.Call(nic.vpcName, func() error {
for _, n := range d.nics {
if err := iptables.AddMetadataRedirect(n.ip, nic.interfaceIP, d.metadataPort); err != nil {
return fmt.Errorf("interface %d: %w", n.index, err)
}
}
return nil
}); err != nil {
return fmt.Errorf("add metadata redirect: %w", err)
}
if err := writeDHCPFiles(d, name); err != nil {
return err
}
if err := metadata.StartMetadata(metadata.NoCloudConfig{
Name: name,
VpcName: nic.vpcName,
BindIP: nic.interfaceIP,
BindPort: d.metadataPort,
Password: d.password,
SSHKEY: d.sshkey,
Documents: d.documents,
}, cfg, false); err != nil {
return fmt.Errorf("start metadata: %w", err)
}
qDisks := make([]qemu.DiskConfig, len(d.disks))
for i, disk := range d.disks {
qDisks[i] = qemu.DiskConfig{Path: disk.path, Dev: disk.dev}
}
qNICs := make([]qemu.NICConfig, len(d.nics))
for i, n := range d.nics {
qNICs[i] = qemu.NICConfig{TapID: n.tapID, Mac: n.mac}
}
qcfg := qemu.Config{
Name: name,
NICs: qNICs,
Disks: qDisks,
Memory: d.memory,
CPUs: d.cpus,
SerialDir: cfg.QEMU.SerialDir,
MonitorDir: cfg.QEMU.MonitorDir,
QMPDir: cfg.QEMU.QMPDir,
}
if d.uefi {
varsPath := filepath.Join(cfg.QEMU.UEFIVarsDir, name+"-uefi-vars.fd")
if err := copyFile(cfg.QEMU.OVMFVarsTemplate, varsPath); err != nil {
return fmt.Errorf("copy uefi vars: %w", err)
}
qcfg.UEFICodePath = cfg.QEMU.OVMFCodePath
qcfg.UEFIVarsPath = varsPath
}
if err := netns.Call(nic.vpcName, func() error {
return qemu.Start(qcfg)
}); err != nil {
return fmt.Errorf("start qemu: %w", err)
}
return state.Set(db, "vm/"+name, state.Running)
}
// writeDHCPFiles écrit, pour chaque subnet touché par la VM, les réservations
// de ses interfaces et les options qui suppriment la route par défaut sur les
// interfaces non primaires. Le subnet de l'interface primaire ne reçoit aucune
// option : les options non taggées du subnet portent déjà la route par défaut.
func writeDHCPFiles(d vmData, name string) error {
type subnetFiles struct {
nic nicData
reservations []dhcp.Reservation
tags []string
}
bySubnet := make(map[string]*subnetFiles)
for _, n := range d.nics {
confName := n.vpcName + "_" + n.bridge
if bySubnet[confName] == nil {
bySubnet[confName] = &subnetFiles{nic: n}
}
f := bySubnet[confName]
f.reservations = append(f.reservations, dhcp.Reservation{
MAC: n.mac, IP: n.ip, Tag: nicTag(name, n.index),
})
if !n.primary {
f.tags = append(f.tags, nicTag(name, n.index))
}
}
for confName, f := range bySubnet {
if err := dhcp.WriteReservations(dhcp.DefaultConfDir, confName, name, f.reservations); err != nil {
return fmt.Errorf("write dhcp reservations on %s: %w", confName, err)
}
if err := dhcp.WriteVMOptions(dhcp.DefaultConfDir, confName, name, f.tags, dhcp.Config{
InterfaceIP: net.ParseIP(f.nic.interfaceIP),
VPCRoute: f.nic.vpcCIDR,
}); err != nil {
return fmt.Errorf("write dhcp options on %s: %w", confName, err)
}
}
return nil
}
// nicTag identifie une interface auprès de dnsmasq. Il est par interface et non
// par VM : deux interfaces d'une même VM peuvent partager un subnet, et n'y
// avoir pas le même rôle.
func nicTag(vmName string, index int) string {
return fmt.Sprintf("%s-%d", vmName, index)
}
func copyFile(src, dst string) error {
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return err
}
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
return out.Sync()
}