f-33: vm: start vms with multiple nic
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
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>
This commit is contained in:
parent
0237acba81
commit
26d5805747
8 changed files with 309 additions and 25 deletions
|
|
@ -10,6 +10,7 @@ import (
|
||||||
type Reservation struct {
|
type Reservation struct {
|
||||||
MAC string
|
MAC string
|
||||||
IP string
|
IP string
|
||||||
|
Tag string // pose set:<Tag> sur l'entrée, pour cibler les options par interface
|
||||||
}
|
}
|
||||||
|
|
||||||
func HostsDir(confDir, name string) string {
|
func HostsDir(confDir, name string) string {
|
||||||
|
|
@ -39,7 +40,11 @@ func WriteReservations(confDir, name, vmName string, res []Reservation) error {
|
||||||
if r.MAC == "" || r.IP == "" {
|
if r.MAC == "" || r.IP == "" {
|
||||||
return fmt.Errorf("incomplete reservation for vm %q: mac=%q ip=%q", vmName, r.MAC, r.IP)
|
return fmt.Errorf("incomplete reservation for vm %q: mac=%q ip=%q", vmName, r.MAC, r.IP)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&sb, "%s,%s\n", r.MAC, r.IP)
|
if r.Tag == "" {
|
||||||
|
fmt.Fprintf(&sb, "%s,%s\n", r.MAC, r.IP)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&sb, "%s,%s,set:%s\n", r.MAC, r.IP, r.Tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
path := filepath.Join(dir, vmName)
|
path := filepath.Join(dir, vmName)
|
||||||
|
|
@ -49,6 +54,51 @@ func WriteReservations(confDir, name, vmName string, res []Reservation) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteVMOptions écrit les options DHCP propres à des interfaces de cette VM
|
||||||
|
// sur ce subnet : elles suppriment la route par défaut — option 3 nue — et
|
||||||
|
// réémettent les autres routes.
|
||||||
|
//
|
||||||
|
// Un override de l'option 121 remplace la précédente en entier, il ne s'y
|
||||||
|
// ajoute pas (vérifié sur dnsmasq 2.90) : omettre la route vers le serveur de
|
||||||
|
// métadonnées la ferait disparaître, et la VM ne se provisionnerait pas.
|
||||||
|
func WriteVMOptions(confDir, name, vmName string, tags []string, c Config) error {
|
||||||
|
if len(tags) == 0 {
|
||||||
|
return RemoveVMOptions(confDir, name, vmName)
|
||||||
|
}
|
||||||
|
if c.InterfaceIP == nil {
|
||||||
|
return fmt.Errorf("interface ip is required to build options for vm %q", vmName)
|
||||||
|
}
|
||||||
|
if c.DefaultGateway != nil {
|
||||||
|
return fmt.Errorf("vm options for %q must not carry a default route", vmName)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := OptsDir(confDir, name)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("create %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
routes := strings.Join(classlessRoutes(c), ",")
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, tag := range tags {
|
||||||
|
fmt.Fprintf(&sb, "tag:%s,3\n", tag)
|
||||||
|
fmt.Fprintf(&sb, "tag:%s,121,%s\n", tag, routes)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(dir, vmName)
|
||||||
|
if err := os.WriteFile(path, []byte(sb.String()), 0644); err != nil {
|
||||||
|
return fmt.Errorf("write %s: %w", path, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveVMOptions(confDir, name, vmName string) error {
|
||||||
|
path := filepath.Join(OptsDir(confDir, name), vmName)
|
||||||
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("remove %s: %w", path, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func RemoveReservations(confDir, name, vmName string) error {
|
func RemoveReservations(confDir, name, vmName string) error {
|
||||||
for _, dir := range []string{HostsDir(confDir, name), OptsDir(confDir, name)} {
|
for _, dir := range []string{HostsDir(confDir, name), OptsDir(confDir, name)} {
|
||||||
path := filepath.Join(dir, vmName)
|
path := filepath.Join(dir, vmName)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package dhcp
|
package dhcp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -124,3 +126,103 @@ func TestUnitName(t *testing.T) {
|
||||||
t.Errorf("unit attendue dnsmasq@%s.service, obtenu %s", subName, got)
|
t.Errorf("unit attendue dnsmasq@%s.service, obtenu %s", subName, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- options par interface ---
|
||||||
|
|
||||||
|
func TestWriteReservations_WithTags(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
res := []Reservation{
|
||||||
|
{MAC: "00:22:33:00:01:02", IP: "10.1.1.2", Tag: "i-web-0"},
|
||||||
|
{MAC: "00:22:33:00:02:07", IP: "10.1.2.7", Tag: "i-web-1"},
|
||||||
|
}
|
||||||
|
if err := WriteReservations(dir, subName, "i-web", res); err != nil {
|
||||||
|
t.Fatalf("WriteReservations : %v", err)
|
||||||
|
}
|
||||||
|
content, _ := os.ReadFile(filepath.Join(HostsDir(dir, subName), "i-web"))
|
||||||
|
want := "00:22:33:00:01:02,10.1.1.2,set:i-web-0\n00:22:33:00:02:07,10.1.2.7,set:i-web-1\n"
|
||||||
|
if string(content) != want {
|
||||||
|
t.Errorf("attendu %q, obtenu %q", want, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func vmOptions(t *testing.T, dir string, tags []string, vpcRoute *net.IPNet) string {
|
||||||
|
t.Helper()
|
||||||
|
if err := WriteVMOptions(dir, subName, "i-web", tags, Config{
|
||||||
|
InterfaceIP: net.ParseIP("10.1.1.1").To4(),
|
||||||
|
VPCRoute: vpcRoute,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("WriteVMOptions : %v", err)
|
||||||
|
}
|
||||||
|
b, err := os.ReadFile(filepath.Join(OptsDir(dir, subName), "i-web"))
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteVMOptions_SuppressesDefaultRoute(t *testing.T) {
|
||||||
|
_, vpcNet, _ := net.ParseCIDR("192.168.0.0/16")
|
||||||
|
got := vmOptions(t, t.TempDir(), []string{"i-web-1"}, vpcNet)
|
||||||
|
|
||||||
|
if !strings.Contains(got, "tag:i-web-1,3\n") {
|
||||||
|
t.Errorf("l'option 3 nue doit supprimer la route par défaut :\n%s", got)
|
||||||
|
}
|
||||||
|
if strings.Contains(got, "0.0.0.0/0") {
|
||||||
|
t.Errorf("la route par défaut ne doit pas figurer dans l'override :\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteVMOptions_ReEmitsMetadataRoute(t *testing.T) {
|
||||||
|
_, vpcNet, _ := net.ParseCIDR("192.168.0.0/16")
|
||||||
|
got := vmOptions(t, t.TempDir(), []string{"i-web-1"}, vpcNet)
|
||||||
|
|
||||||
|
if !strings.Contains(got, "169.254.169.254/32,10.1.1.1") {
|
||||||
|
t.Errorf("un override de l'option 121 remplace la précédente en entier : sans la route metadata, la VM ne se provisionne pas\n%s", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "192.168.0.0/16,10.1.1.1") {
|
||||||
|
t.Errorf("la route VPC doit être réémise elle aussi :\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteVMOptions_BridgeHasNoVPCRoute(t *testing.T) {
|
||||||
|
got := vmOptions(t, t.TempDir(), []string{"i-web-1"}, nil)
|
||||||
|
|
||||||
|
if !strings.Contains(got, "169.254.169.254/32") {
|
||||||
|
t.Errorf("route metadata absente :\n%s", got)
|
||||||
|
}
|
||||||
|
if strings.Contains(got, "192.168") {
|
||||||
|
t.Errorf("aucune route VPC attendue en mode bridge :\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteVMOptions_OneBlockPerTag(t *testing.T) {
|
||||||
|
got := vmOptions(t, t.TempDir(), []string{"i-web-1", "i-web-2"}, nil)
|
||||||
|
|
||||||
|
for _, tag := range []string{"tag:i-web-1,3", "tag:i-web-2,3"} {
|
||||||
|
if !strings.Contains(got, tag) {
|
||||||
|
t.Errorf("%q absent :\n%s", tag, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteVMOptions_NoTagRemovesFile(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
_ = vmOptions(t, dir, []string{"i-web-1"}, nil)
|
||||||
|
|
||||||
|
if err := WriteVMOptions(dir, subName, "i-web", nil, Config{}); err != nil {
|
||||||
|
t.Fatalf("WriteVMOptions : %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(OptsDir(dir, subName), "i-web")); !os.IsNotExist(err) {
|
||||||
|
t.Error("sans interface non primaire, aucun fichier d'options ne doit subsister")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteVMOptions_RefusesDefaultGateway(t *testing.T) {
|
||||||
|
err := WriteVMOptions(t.TempDir(), subName, "i-web", []string{"i-web-1"}, Config{
|
||||||
|
InterfaceIP: net.ParseIP("10.1.1.1").To4(),
|
||||||
|
DefaultGateway: net.ParseIP("10.1.1.254").To4(),
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("ces options servent à retirer la route par défaut : en porter une est une incohérence")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,16 @@ type DiskConfig struct {
|
||||||
Dev string
|
Dev string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NICConfig décrit une interface réseau. Sa position dans Config.NICs
|
||||||
|
// détermine le slot PCI, donc le nom de l'interface dans le guest.
|
||||||
|
type NICConfig struct {
|
||||||
|
TapID int
|
||||||
|
Mac string
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Name string
|
Name string
|
||||||
TapID int
|
NICs []NICConfig
|
||||||
Mac string
|
|
||||||
Disks []DiskConfig
|
Disks []DiskConfig
|
||||||
Memory int
|
Memory int
|
||||||
CPUs int
|
CPUs int
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
firstNICSlot = 0x03
|
||||||
|
lastNICSlot = 0x1d
|
||||||
|
maxNICs = lastNICSlot - firstNICSlot + 1
|
||||||
|
)
|
||||||
|
|
||||||
func Start(cfg Config) error {
|
func Start(cfg Config) error {
|
||||||
memory := cfg.Memory
|
memory := cfg.Memory
|
||||||
if memory == 0 {
|
if memory == 0 {
|
||||||
|
|
@ -97,11 +103,24 @@ func Start(cfg Config) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args,
|
// Slots 0x03 à 0x1d réservés au réseau par la carte PCI (#36). Le slot est
|
||||||
"-netdev", fmt.Sprintf("tap,id=net0,ifname=tap%d,script=no,downscript=no", cfg.TapID),
|
// dérivé de l'index et non laissé à QEMU : c'est lui qui fixe le nom de
|
||||||
"-device", fmt.Sprintf("virtio-net-pci,netdev=net0,mac=%s,bus=pci.0,addr=0x03", cfg.Mac),
|
// l'interface dans le guest, et un slot flottant la renomme d'un démarrage
|
||||||
"-daemonize",
|
// à l'autre.
|
||||||
)
|
if len(cfg.NICs) == 0 {
|
||||||
|
return fmt.Errorf("vm %s has no network interface", cfg.Name)
|
||||||
|
}
|
||||||
|
if len(cfg.NICs) > maxNICs {
|
||||||
|
return fmt.Errorf("vm %s has %d interfaces, the pci map holds %d", cfg.Name, len(cfg.NICs), maxNICs)
|
||||||
|
}
|
||||||
|
for i, n := range cfg.NICs {
|
||||||
|
args = append(args,
|
||||||
|
"-netdev", fmt.Sprintf("tap,id=net%d,ifname=tap%d,script=no,downscript=no", i, n.TapID),
|
||||||
|
"-device", fmt.Sprintf("virtio-net-pci,netdev=net%d,mac=%s,bus=pci.0,addr=0x%02x", i, n.Mac, firstNICSlot+i),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, "-daemonize")
|
||||||
|
|
||||||
scopeArgs := append([]string{
|
scopeArgs := append([]string{
|
||||||
"--scope",
|
"--scope",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package vm
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
|
@ -33,19 +34,28 @@ func StartVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
||||||
}
|
}
|
||||||
nic := d.primary()
|
nic := d.primary()
|
||||||
|
|
||||||
if err := netif.CreateTap(nic.tapID, nic.bridge, nic.vpcName); err != nil {
|
for _, n := range d.nics {
|
||||||
return fmt.Errorf("create tap: %w", err)
|
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 {
|
if err := netns.Call(nic.vpcName, func() error {
|
||||||
return iptables.AddMetadataRedirect(nic.ip, nic.interfaceIP, d.metadataPort)
|
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 {
|
}); err != nil {
|
||||||
return fmt.Errorf("add metadata redirect: %w", err)
|
return fmt.Errorf("add metadata redirect: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dhcp.WriteReservations(dhcp.DefaultConfDir, nic.vpcName+"_"+nic.bridge, name,
|
if err := writeDHCPFiles(d, name); err != nil {
|
||||||
[]dhcp.Reservation{{MAC: nic.mac, IP: nic.ip}}); err != nil {
|
return err
|
||||||
return fmt.Errorf("write dhcp reservation: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := metadata.StartMetadata(metadata.NoCloudConfig{
|
if err := metadata.StartMetadata(metadata.NoCloudConfig{
|
||||||
|
|
@ -65,10 +75,14 @@ func StartVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
||||||
qDisks[i] = qemu.DiskConfig{Path: disk.path, Dev: disk.dev}
|
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{
|
qcfg := qemu.Config{
|
||||||
Name: name,
|
Name: name,
|
||||||
TapID: nic.tapID,
|
NICs: qNICs,
|
||||||
Mac: nic.mac,
|
|
||||||
Disks: qDisks,
|
Disks: qDisks,
|
||||||
Memory: d.memory,
|
Memory: d.memory,
|
||||||
CPUs: d.cpus,
|
CPUs: d.cpus,
|
||||||
|
|
@ -95,6 +109,53 @@ func StartVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
||||||
return state.Set(db, "vm/"+name, state.Running)
|
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 {
|
func copyFile(src, dst string) error {
|
||||||
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package vm
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -27,6 +28,8 @@ type nicData struct {
|
||||||
mac string
|
mac string
|
||||||
tapID int
|
tapID int
|
||||||
primary bool
|
primary bool
|
||||||
|
mode string
|
||||||
|
vpcCIDR *net.IPNet
|
||||||
}
|
}
|
||||||
|
|
||||||
type vmData struct {
|
type vmData struct {
|
||||||
|
|
@ -190,6 +193,23 @@ func loadNIC(db *badger.DB, name string, idx int, entries map[string]string) (ni
|
||||||
}
|
}
|
||||||
n.interfaceIP = interfaceIP
|
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"]
|
n.ip = entries[prefix+"ip"]
|
||||||
if n.ip == "" {
|
if n.ip == "" {
|
||||||
return n, fmt.Errorf("nic %d of vm %s has no ip", idx, name)
|
return n, fmt.Errorf("nic %d of vm %s has no ip", idx, name)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ func newVMInDB(t *testing.T) *badger.DB {
|
||||||
kv.AddInDB(db, "vm/vm-1/nic/0/primary", "true")
|
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/vpc", "vp-admin")
|
||||||
kv.AddInDB(db, "subnet/sn-000001/interface_ip", "10.1.1.1")
|
kv.AddInDB(db, "subnet/sn-000001/interface_ip", "10.1.1.1")
|
||||||
|
kv.AddInDB(db, "subnet/sn-000001/mode", "vxlan")
|
||||||
|
kv.AddInDB(db, "vpc/vp-admin/cidr", "192.168.0.0/16")
|
||||||
kv.AddInDB(db, "subnet/sn-000001/dhcp/10.1.1.2", "00:22:33:00:01:02")
|
kv.AddInDB(db, "subnet/sn-000001/dhcp/10.1.1.2", "00:22:33:00:01:02")
|
||||||
kv.AddInDB(db, "vm/vm-1/nic/0/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/metadata_port", "8081")
|
||||||
|
|
@ -104,6 +106,8 @@ func addNIC(t *testing.T, db *badger.DB, idx int, subnet, ip, mac string, primar
|
||||||
}
|
}
|
||||||
kv.AddInDB(db, "subnet/"+subnet+"/vpc", "vp-admin")
|
kv.AddInDB(db, "subnet/"+subnet+"/vpc", "vp-admin")
|
||||||
kv.AddInDB(db, "subnet/"+subnet+"/interface_ip", "10.1.1.1")
|
kv.AddInDB(db, "subnet/"+subnet+"/interface_ip", "10.1.1.1")
|
||||||
|
kv.AddInDB(db, "subnet/"+subnet+"/mode", "vxlan")
|
||||||
|
kv.AddInDB(db, "vpc/vp-admin/cidr", "192.168.0.0/16")
|
||||||
kv.AddInDB(db, "subnet/"+subnet+"/dhcp/"+ip, mac)
|
kv.AddInDB(db, "subnet/"+subnet+"/dhcp/"+ip, mac)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,6 +174,7 @@ func TestLoadVM_NoPrimaryIsAnError(t *testing.T) {
|
||||||
kv.AddInDB(db, "vm/vm-1/memory", "2048")
|
kv.AddInDB(db, "vm/vm-1/memory", "2048")
|
||||||
kv.AddInDB(db, "vm/vm-1/cpus", "2")
|
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)
|
addNIC(t, db, 0, "sn-000001", "10.1.1.2", "00:22:33:00:01:02", false)
|
||||||
|
kv.AddInDB(db, "subnet/sn-000001/mode", "vxlan")
|
||||||
|
|
||||||
if _, err := loadVM(db, "vm-1"); err == nil {
|
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")
|
t.Error("aucune interface primaire : loadVM doit échouer plutôt que de laisser StartVM choisir au hasard")
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,12 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
||||||
// socket absent ou QEMU déjà arrêté : cleanup direct
|
// socket absent ou QEMU déjà arrêté : cleanup direct
|
||||||
|
|
||||||
if err := netns.Call(nic.vpcName, func() error {
|
if err := netns.Call(nic.vpcName, func() error {
|
||||||
return iptables.DeleteMetadataRedirect(nic.ip, nic.interfaceIP, d.metadataPort)
|
for _, n := range d.nics {
|
||||||
|
if err := iptables.DeleteMetadataRedirect(n.ip, nic.interfaceIP, d.metadataPort); err != nil {
|
||||||
|
return fmt.Errorf("interface %d: %w", n.index, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("delete metadata redirect: %w", err)
|
return fmt.Errorf("delete metadata redirect: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -58,11 +63,13 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
||||||
return fmt.Errorf("stop metadata: %w", err)
|
return fmt.Errorf("stop metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := netif.DeleteTap(nic.tapID, nic.vpcName); err != nil {
|
for _, n := range d.nics {
|
||||||
return fmt.Errorf("delete tap: %w", err)
|
if err := netif.DeleteTap(n.tapID, n.vpcName); err != nil {
|
||||||
|
return fmt.Errorf("delete tap of interface %d: %w", n.index, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := removeDHCPReservation(nic, name); err != nil {
|
if err := removeDHCPFiles(d, name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,12 +81,26 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error {
|
||||||
return state.Set(db, "vm/"+name, state.Deleted)
|
return state.Set(db, "vm/"+name, state.Deleted)
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeDHCPReservation retire le fichier de réservation puis redémarre dnsmasq :
|
// removeDHCPFiles retire les fichiers de la VM dans chaque subnet qu'elle
|
||||||
// un fichier ajouté dans un dhcp-hostsdir est relu à chaud, un fichier retiré ne
|
// touche, puis redémarre les dnsmasq concernés : un fichier ajouté dans un
|
||||||
// l'est pas (vérifié sur dnsmasq 2.90).
|
// dhcp-hostsdir est relu à chaud, un fichier retiré ne l'est pas (vérifié sur
|
||||||
func removeDHCPReservation(nic nicData, name string) error {
|
// dnsmasq 2.90).
|
||||||
confName := nic.vpcName + "_" + nic.bridge
|
func removeDHCPFiles(d vmData, name string) error {
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, n := range d.nics {
|
||||||
|
confName := n.vpcName + "_" + n.bridge
|
||||||
|
if seen[confName] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[confName] = true
|
||||||
|
if err := removeDHCPReservation(confName, name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeDHCPReservation(confName, name string) error {
|
||||||
if err := dhcp.RemoveReservations(dhcp.DefaultConfDir, confName, name); err != nil {
|
if err := dhcp.RemoveReservations(dhcp.DefaultConfDir, confName, name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue