f-33: net: add multi net in dispatch et vms #33
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
parent
1ec1d44c1a
commit
0237acba81
15 changed files with 753 additions and 146 deletions
|
|
@ -31,26 +31,27 @@ func StartVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nic := d.primary()
|
||||
|
||||
if err := netif.CreateTap(d.tapID, d.bridge, d.vpcName); err != nil {
|
||||
if err := netif.CreateTap(nic.tapID, nic.bridge, nic.vpcName); err != nil {
|
||||
return fmt.Errorf("create tap: %w", err)
|
||||
}
|
||||
|
||||
if err := netns.Call(d.vpcName, func() error {
|
||||
return iptables.AddMetadataRedirect(d.ip, d.interfaceIP, d.metadataPort)
|
||||
if err := netns.Call(nic.vpcName, func() error {
|
||||
return iptables.AddMetadataRedirect(nic.ip, nic.interfaceIP, d.metadataPort)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("add metadata redirect: %w", err)
|
||||
}
|
||||
|
||||
if err := dhcp.WriteReservations(dhcp.DefaultConfDir, d.vpcName+"_"+d.bridge, name,
|
||||
[]dhcp.Reservation{{MAC: d.mac, IP: d.ip}}); err != nil {
|
||||
if err := dhcp.WriteReservations(dhcp.DefaultConfDir, nic.vpcName+"_"+nic.bridge, name,
|
||||
[]dhcp.Reservation{{MAC: nic.mac, IP: nic.ip}}); err != nil {
|
||||
return fmt.Errorf("write dhcp reservation: %w", err)
|
||||
}
|
||||
|
||||
if err := metadata.StartMetadata(metadata.NoCloudConfig{
|
||||
Name: name,
|
||||
VpcName: d.vpcName,
|
||||
BindIP: d.interfaceIP,
|
||||
VpcName: nic.vpcName,
|
||||
BindIP: nic.interfaceIP,
|
||||
BindPort: d.metadataPort,
|
||||
Password: d.password,
|
||||
SSHKEY: d.sshkey,
|
||||
|
|
@ -66,8 +67,8 @@ func StartVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
|||
|
||||
qcfg := qemu.Config{
|
||||
Name: name,
|
||||
TapID: d.tapID,
|
||||
Mac: d.mac,
|
||||
TapID: nic.tapID,
|
||||
Mac: nic.mac,
|
||||
Disks: qDisks,
|
||||
Memory: d.memory,
|
||||
CPUs: d.cpus,
|
||||
|
|
@ -85,7 +86,7 @@ func StartVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
|||
qcfg.UEFIVarsPath = varsPath
|
||||
}
|
||||
|
||||
if err := netns.Call(d.vpcName, func() error {
|
||||
if err := netns.Call(nic.vpcName, func() error {
|
||||
return qemu.Start(qcfg)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("start qemu: %w", err)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package vm
|
|||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
|
@ -16,15 +17,21 @@ type diskEntry struct {
|
|||
dev string
|
||||
}
|
||||
|
||||
type nicData struct {
|
||||
index int
|
||||
subnetName string
|
||||
vpcName string
|
||||
bridge string
|
||||
interfaceIP string
|
||||
ip string
|
||||
mac string
|
||||
tapID int
|
||||
primary bool
|
||||
}
|
||||
|
||||
type vmData struct {
|
||||
subnetName string
|
||||
vpcName string
|
||||
interfaceIP string
|
||||
bridge string
|
||||
tapID int
|
||||
ip string
|
||||
nics []nicData
|
||||
metadataPort string
|
||||
mac string
|
||||
disks []diskEntry
|
||||
memory int
|
||||
cpus int
|
||||
|
|
@ -34,47 +41,25 @@ type vmData struct {
|
|||
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
|
||||
|
||||
subnetName, err := kv.GetFromDB(db, "vm/"+name+"/subnet")
|
||||
nics, err := loadNICs(db, name)
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("get subnet: %w", err)
|
||||
return d, 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
|
||||
|
||||
interfaceIP, err := kv.GetFromDB(db, "subnet/"+subnetName+"/interface_ip")
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("get interface_ip: %w", err)
|
||||
}
|
||||
d.interfaceIP = interfaceIP
|
||||
|
||||
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
|
||||
d.nics = nics
|
||||
|
||||
metadataPort, err := kv.GetFromDB(db, "vm/"+name+"/metadata_port")
|
||||
if err != nil {
|
||||
|
|
@ -82,12 +67,6 @@ func loadVM(db *badger.DB, name string) (vmData, error) {
|
|||
}
|
||||
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
|
||||
|
||||
diskEntries, err := kv.ListByPrefix(db, "vm/"+name+"/disk/")
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("list disks: %w", err)
|
||||
|
|
@ -139,3 +118,101 @@ func loadVM(db *badger.DB, name string) (vmData, error) {
|
|||
|
||||
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.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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package vm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||
|
|
@ -12,11 +14,12 @@ func newVMInDB(t *testing.T) *badger.DB {
|
|||
db := kv.InitDB(kv.Config{Path: t.TempDir()}, false)
|
||||
t.Cleanup(func() { db.Close() })
|
||||
|
||||
kv.AddInDB(db, "vm/vm-1/subnet", "sn-000001")
|
||||
kv.AddInDB(db, "vm/vm-1/nic/0/subnet", "sn-000001")
|
||||
kv.AddInDB(db, "vm/vm-1/nic/0/primary", "true")
|
||||
kv.AddInDB(db, "subnet/sn-000001/vpc", "vp-admin")
|
||||
kv.AddInDB(db, "subnet/sn-000001/interface_ip", "10.1.1.1")
|
||||
kv.AddInDB(db, "subnet/sn-000001/dhcp/10.1.1.2", "00:22:33:00:01:02")
|
||||
kv.AddInDB(db, "vm/vm-1/ip", "10.1.1.2")
|
||||
kv.AddInDB(db, "vm/vm-1/nic/0/ip", "10.1.1.2")
|
||||
kv.AddInDB(db, "vm/vm-1/metadata_port", "8081")
|
||||
kv.AddInDB(db, "vm/vm-1/disk/vda", "/data/root.qcow2")
|
||||
kv.AddInDB(db, "vm/vm-1/memory", "2048")
|
||||
|
|
@ -88,3 +91,106 @@ func TestLoadVM_EmptyDocumentIsPreserved(t *testing.T) {
|
|||
t.Errorf("contenu attendu vide, obtenu %q", content)
|
||||
}
|
||||
}
|
||||
|
||||
// --- multi-interfaces ---
|
||||
|
||||
func addNIC(t *testing.T, db *badger.DB, idx int, subnet, ip, mac string, primary bool) {
|
||||
t.Helper()
|
||||
prefix := fmt.Sprintf("vm/vm-1/nic/%d/", idx)
|
||||
kv.AddInDB(db, prefix+"subnet", subnet)
|
||||
kv.AddInDB(db, prefix+"ip", ip)
|
||||
if primary {
|
||||
kv.AddInDB(db, prefix+"primary", "true")
|
||||
}
|
||||
kv.AddInDB(db, "subnet/"+subnet+"/vpc", "vp-admin")
|
||||
kv.AddInDB(db, "subnet/"+subnet+"/interface_ip", "10.1.1.1")
|
||||
kv.AddInDB(db, "subnet/"+subnet+"/dhcp/"+ip, mac)
|
||||
}
|
||||
|
||||
func TestLoadVM_SingleNIC(t *testing.T) {
|
||||
d, err := loadVM(newVMInDB(t), "vm-1")
|
||||
if err != nil {
|
||||
t.Fatalf("loadVM : %v", err)
|
||||
}
|
||||
if len(d.nics) != 1 {
|
||||
t.Fatalf("1 interface attendue, obtenu %d", len(d.nics))
|
||||
}
|
||||
if !d.primary().primary || d.primary().ip != "10.1.1.2" {
|
||||
t.Errorf("primaire inattendue : %+v", d.primary())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVM_MultipleNICsSortedByIndex(t *testing.T) {
|
||||
db := newVMInDB(t)
|
||||
addNIC(t, db, 2, "sn-000003", "10.3.0.9", "00:22:33:00:03:09", false)
|
||||
addNIC(t, db, 1, "sn-000002", "10.2.0.5", "00:22:33:00:02:05", false)
|
||||
|
||||
d, err := loadVM(db, "vm-1")
|
||||
if err != nil {
|
||||
t.Fatalf("loadVM : %v", err)
|
||||
}
|
||||
if len(d.nics) != 3 {
|
||||
t.Fatalf("3 interfaces attendues, obtenu %d", len(d.nics))
|
||||
}
|
||||
for i, n := range d.nics {
|
||||
if n.index != i {
|
||||
t.Errorf("interface en position %d porte l'index %d — l'ordre détermine le slot PCI", i, n.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVM_TapAllocatedPerNIC(t *testing.T) {
|
||||
db := newVMInDB(t)
|
||||
addNIC(t, db, 1, "sn-000002", "10.2.0.5", "00:22:33:00:02:05", false)
|
||||
|
||||
d, err := loadVM(db, "vm-1")
|
||||
if err != nil {
|
||||
t.Fatalf("loadVM : %v", err)
|
||||
}
|
||||
if d.nics[0].tapID == d.nics[1].tapID {
|
||||
t.Errorf("deux interfaces partagent le tap %d", d.nics[0].tapID)
|
||||
}
|
||||
for _, n := range d.nics {
|
||||
stored, err := kv.GetFromDB(db, fmt.Sprintf("vm/vm-1/nic/%d/tap_id", n.index))
|
||||
if err != nil {
|
||||
t.Errorf("tap_id de l'interface %d non persisté : %v", n.index, err)
|
||||
continue
|
||||
}
|
||||
if stored != strconv.Itoa(n.tapID) {
|
||||
t.Errorf("tap_id de l'interface %d : %q en base, %d en mémoire", n.index, stored, n.tapID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVM_NoPrimaryIsAnError(t *testing.T) {
|
||||
db := kv.InitDB(kv.Config{Path: t.TempDir()}, false)
|
||||
t.Cleanup(func() { db.Close() })
|
||||
kv.AddInDB(db, "vm/vm-1/metadata_port", "8081")
|
||||
kv.AddInDB(db, "vm/vm-1/disk/vda", "/data/root.qcow2")
|
||||
kv.AddInDB(db, "vm/vm-1/memory", "2048")
|
||||
kv.AddInDB(db, "vm/vm-1/cpus", "2")
|
||||
addNIC(t, db, 0, "sn-000001", "10.1.1.2", "00:22:33:00:01:02", false)
|
||||
|
||||
if _, err := loadVM(db, "vm-1"); err == nil {
|
||||
t.Error("aucune interface primaire : loadVM doit échouer plutôt que de laisser StartVM choisir au hasard")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVM_TwoPrimariesIsAnError(t *testing.T) {
|
||||
db := newVMInDB(t)
|
||||
addNIC(t, db, 1, "sn-000002", "10.2.0.5", "00:22:33:00:02:05", true)
|
||||
|
||||
if _, err := loadVM(db, "vm-1"); err == nil {
|
||||
t.Error("deux interfaces primaires doivent être refusées")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVM_NoNICIsAnError(t *testing.T) {
|
||||
db := kv.InitDB(kv.Config{Path: t.TempDir()}, false)
|
||||
t.Cleanup(func() { db.Close() })
|
||||
kv.AddInDB(db, "vm/vm-1/memory", "2048")
|
||||
|
||||
if _, err := loadVM(db, "vm-1"); err == nil {
|
||||
t.Error("une VM sans interface doit être refusée")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nic := d.primary()
|
||||
|
||||
socketPath := filepath.Join(cfg.QEMU.QMPDir, name+".sock")
|
||||
|
||||
|
|
@ -47,8 +48,8 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
|||
}
|
||||
// socket absent ou QEMU déjà arrêté : cleanup direct
|
||||
|
||||
if err := netns.Call(d.vpcName, func() error {
|
||||
return iptables.DeleteMetadataRedirect(d.ip, d.interfaceIP, d.metadataPort)
|
||||
if err := netns.Call(nic.vpcName, func() error {
|
||||
return iptables.DeleteMetadataRedirect(nic.ip, nic.interfaceIP, d.metadataPort)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("delete metadata redirect: %w", err)
|
||||
}
|
||||
|
|
@ -57,11 +58,11 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
|||
return fmt.Errorf("stop metadata: %w", err)
|
||||
}
|
||||
|
||||
if err := netif.DeleteTap(d.tapID, d.vpcName); err != nil {
|
||||
if err := netif.DeleteTap(nic.tapID, nic.vpcName); err != nil {
|
||||
return fmt.Errorf("delete tap: %w", err)
|
||||
}
|
||||
|
||||
if err := removeDHCPReservation(d, name); err != nil {
|
||||
if err := removeDHCPReservation(nic, name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -76,8 +77,8 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
|||
// removeDHCPReservation retire le fichier de réservation puis redémarre dnsmasq :
|
||||
// un fichier ajouté dans un dhcp-hostsdir est relu à chaud, un fichier retiré ne
|
||||
// l'est pas (vérifié sur dnsmasq 2.90).
|
||||
func removeDHCPReservation(d vmData, name string) error {
|
||||
confName := d.vpcName + "_" + d.bridge
|
||||
func removeDHCPReservation(nic nicData, name string) error {
|
||||
confName := nic.vpcName + "_" + nic.bridge
|
||||
|
||||
if err := dhcp.RemoveReservations(dhcp.DefaultConfDir, confName, name); err != nil {
|
||||
return err
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue