From da0d043846d7611d3ae90c2c12858c22ae4d937c Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 16:29:45 +0200 Subject: [PATCH 01/12] f-25: qemu: add handle gestion Signed-off-by: GnomeZworc --- internal/qemu/start_linux.go | 48 ++++++++++++++++++++++++++++++++++++ internal/qemu/start_other.go | 14 +++++++++++ 2 files changed, 62 insertions(+) create mode 100644 internal/qemu/start_linux.go create mode 100644 internal/qemu/start_other.go diff --git a/internal/qemu/start_linux.go b/internal/qemu/start_linux.go new file mode 100644 index 0000000..6119278 --- /dev/null +++ b/internal/qemu/start_linux.go @@ -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 +} diff --git a/internal/qemu/start_other.go b/internal/qemu/start_other.go new file mode 100644 index 0000000..9201a2d --- /dev/null +++ b/internal/qemu/start_other.go @@ -0,0 +1,14 @@ +//go:build !linux + +package qemu + +import "errors" + +type Config struct { + Name, Mac, VolumePath string + TapID, Memory, CPUs int +} + +func Start(cfg Config) error { + return errors.New("vm: not supported on this platform") +} From a5bf748bdfc8e8adc89786b5c194d403511d4674 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 17:27:01 +0200 Subject: [PATCH 02/12] f-25: vm: ajout de la logique de demarrage des vms Signed-off-by: GnomeZworc --- internal/netif/tap_linux.go | 6 ++ internal/netif/tap_other.go | 4 ++ internal/vm/create.go | 108 ++++++++++++++++++++++++++++++++++++ internal/vm/delete.go | 71 ++++++++++++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 internal/vm/create.go create mode 100644 internal/vm/delete.go diff --git a/internal/netif/tap_linux.go b/internal/netif/tap_linux.go index a5abc9c..15313af 100644 --- a/internal/netif/tap_linux.go +++ b/internal/netif/tap_linux.go @@ -26,3 +26,9 @@ func CreateTap(tapID int, bridgeName, vpcName string) error { return LinkSetUp(name) }) } + +func DeleteTap(tapID int, vpcName string) error { + return netns.Call(vpcName, func() error { + return DeleteLink(fmt.Sprintf("tap%d", tapID)) + }) +} diff --git a/internal/netif/tap_other.go b/internal/netif/tap_other.go index a81fefb..18afcbe 100644 --- a/internal/netif/tap_other.go +++ b/internal/netif/tap_other.go @@ -7,3 +7,7 @@ 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") +} diff --git a/internal/vm/create.go b/internal/vm/create.go new file mode 100644 index 0000000..3fc22af --- /dev/null +++ b/internal/vm/create.go @@ -0,0 +1,108 @@ +package vm + +import ( + "fmt" + "strconv" + + "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) error { + state, err := kv.GetFromDB(db, "vm/"+name+"/state") + if err != nil { + return err + } + if state != "starting" { + return nil + } + + vpcName, err := kv.GetFromDB(db, "vm/"+name+"/vpc") + if err != nil { + return fmt.Errorf("get vpc: %w", err) + } + + bridge, err := kv.GetFromDB(db, "vm/"+name+"/bridge") + if err != nil { + return fmt.Errorf("get bridge: %w", err) + } + + tapIDStr, err := kv.GetFromDB(db, "vm/"+name+"/tap_id") + if err != nil { + return fmt.Errorf("get tap_id: %w", err) + } + tapID, err := strconv.Atoi(tapIDStr) + if err != nil { + return fmt.Errorf("parse tap_id: %w", err) + } + + mac, err := kv.GetFromDB(db, "vm/"+name+"/mac") + if err != nil { + return fmt.Errorf("get mac: %w", err) + } + + volumePath, err := kv.GetFromDB(db, "vm/"+name+"/volume_path") + if err != nil { + return fmt.Errorf("get volume_path: %w", err) + } + + memoryStr, err := kv.GetFromDB(db, "vm/"+name+"/memory") + if err != nil { + return fmt.Errorf("get memory: %w", err) + } + memory, _ := strconv.Atoi(memoryStr) + + cpusStr, err := kv.GetFromDB(db, "vm/"+name+"/cpus") + if err != nil { + return fmt.Errorf("get cpus: %w", err) + } + cpus, _ := strconv.Atoi(cpusStr) + + bindIP, err := kv.GetFromDB(db, "vm/"+name+"/metadata_bind_ip") + if err != nil { + return fmt.Errorf("get metadata_bind_ip: %w", err) + } + + bindPort, err := kv.GetFromDB(db, "vm/"+name+"/metadata_bind_port") + if err != nil { + return fmt.Errorf("get metadata_bind_port: %w", err) + } + + password, _ := kv.GetFromDB(db, "vm/"+name+"/password") + sshkey, _ := kv.GetFromDB(db, "vm/"+name+"/sshkey") + + if err := netif.CreateTap(tapID, bridge, vpcName); err != nil { + return fmt.Errorf("create tap: %w", err) + } + + if err := metadata.StartMetadata(metadata.NoCloudConfig{ + Name: name, + VpcName: vpcName, + BindIP: bindIP, + BindPort: bindPort, + Password: password, + SSHKEY: sshkey, + }, db, 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") +} diff --git a/internal/vm/delete.go b/internal/vm/delete.go new file mode 100644 index 0000000..d3f376c --- /dev/null +++ b/internal/vm/delete.go @@ -0,0 +1,71 @@ +package vm + +import ( + "fmt" + "strconv" + "time" + + configuration "git.g3e.fr/syonad/two/internal/config/agent" + "git.g3e.fr/syonad/two/internal/metadata" + "git.g3e.fr/syonad/two/internal/netif" + "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 + } + + vpcName, err := kv.GetFromDB(db, "vm/"+name+"/vpc") + if err != nil { + return fmt.Errorf("get vpc: %w", err) + } + + tapIDStr, err := kv.GetFromDB(db, "vm/"+name+"/tap_id") + if err != nil { + return fmt.Errorf("get tap_id: %w", err) + } + tapID, err := strconv.Atoi(tapIDStr) + if err != nil { + return fmt.Errorf("parse tap_id: %w", 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 := metadata.StopMetadata(name, db, false); err != nil { + return fmt.Errorf("stop metadata: %w", err) + } + + if err := netif.DeleteTap(tapID, vpcName); err != nil { + return fmt.Errorf("delete tap: %w", err) + } + + return kv.AddInDB(db, "vm/"+name+"/state", "stopped") +} From 504a2a723befadb0a69c11ef10e0f86ba4bfa653 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 17:37:25 +0200 Subject: [PATCH 03/12] f-25: vms: add iptables gestion Signed-off-by: GnomeZworc --- internal/iptables/iptables.go | 42 +++++++++++++++++++++++++++++++++++ internal/vm/create.go | 36 ++++++++++++++++++++---------- internal/vm/delete.go | 23 +++++++++++++++++++ 3 files changed, 89 insertions(+), 12 deletions(-) create mode 100644 internal/iptables/iptables.go diff --git a/internal/iptables/iptables.go b/internal/iptables/iptables.go new file mode 100644 index 0000000..1d156a7 --- /dev/null +++ b/internal/iptables/iptables.go @@ -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 +} diff --git a/internal/vm/create.go b/internal/vm/create.go index 3fc22af..243a8d6 100644 --- a/internal/vm/create.go +++ b/internal/vm/create.go @@ -4,6 +4,7 @@ import ( "fmt" "strconv" + "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" @@ -41,6 +42,21 @@ func StartVM(db *badger.DB, name string) error { return fmt.Errorf("parse tap_id: %w", err) } + vmIP, err := kv.GetFromDB(db, "vm/"+name+"/ip") + if err != nil { + return fmt.Errorf("get ip: %w", err) + } + + gatewayIP, err := kv.GetFromDB(db, "vm/"+name+"/gateway_ip") + if err != nil { + return fmt.Errorf("get gateway_ip: %w", err) + } + + metadataPort, err := kv.GetFromDB(db, "vm/"+name+"/metadata_port") + if err != nil { + return fmt.Errorf("get metadata_port: %w", err) + } + mac, err := kv.GetFromDB(db, "vm/"+name+"/mac") if err != nil { return fmt.Errorf("get mac: %w", err) @@ -63,16 +79,6 @@ func StartVM(db *badger.DB, name string) error { } cpus, _ := strconv.Atoi(cpusStr) - bindIP, err := kv.GetFromDB(db, "vm/"+name+"/metadata_bind_ip") - if err != nil { - return fmt.Errorf("get metadata_bind_ip: %w", err) - } - - bindPort, err := kv.GetFromDB(db, "vm/"+name+"/metadata_bind_port") - if err != nil { - return fmt.Errorf("get metadata_bind_port: %w", err) - } - password, _ := kv.GetFromDB(db, "vm/"+name+"/password") sshkey, _ := kv.GetFromDB(db, "vm/"+name+"/sshkey") @@ -80,11 +86,17 @@ func StartVM(db *badger.DB, name string) error { return fmt.Errorf("create tap: %w", err) } + if err := netns.Call(vpcName, func() error { + return iptables.AddMetadataRedirect(vmIP, gatewayIP, metadataPort) + }); err != nil { + return fmt.Errorf("add metadata redirect: %w", err) + } + if err := metadata.StartMetadata(metadata.NoCloudConfig{ Name: name, VpcName: vpcName, - BindIP: bindIP, - BindPort: bindPort, + BindIP: gatewayIP, + BindPort: metadataPort, Password: password, SSHKEY: sshkey, }, db, false); err != nil { diff --git a/internal/vm/delete.go b/internal/vm/delete.go index d3f376c..7e89424 100644 --- a/internal/vm/delete.go +++ b/internal/vm/delete.go @@ -6,8 +6,10 @@ import ( "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" @@ -37,6 +39,21 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error { return fmt.Errorf("parse tap_id: %w", err) } + vmIP, err := kv.GetFromDB(db, "vm/"+name+"/ip") + if err != nil { + return fmt.Errorf("get ip: %w", err) + } + + gatewayIP, err := kv.GetFromDB(db, "vm/"+name+"/gateway_ip") + if err != nil { + return fmt.Errorf("get gateway_ip: %w", err) + } + + metadataPort, err := kv.GetFromDB(db, "vm/"+name+"/metadata_port") + if err != nil { + return fmt.Errorf("get metadata_port: %w", err) + } + socketPath := fmt.Sprintf("/tmp/%s.qmp-sock", name) if _, err := qmp.Send(socketPath, []string{`{"execute":"system_powerdown"}`}); err != nil { @@ -59,6 +76,12 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error { } } + if err := netns.Call(vpcName, func() error { + return iptables.DeleteMetadataRedirect(vmIP, gatewayIP, metadataPort) + }); err != nil { + return fmt.Errorf("delete metadata redirect: %w", err) + } + if err := metadata.StopMetadata(name, db, false); err != nil { return fmt.Errorf("stop metadata: %w", err) } From 2efaf2009834144c886553d0386503c7873b193b Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 17:49:32 +0200 Subject: [PATCH 04/12] f-25: fix: reduce options number Signed-off-by: GnomeZworc --- internal/vm/create.go | 19 +++++++++++-------- internal/vm/delete.go | 17 +++++++++++------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/internal/vm/create.go b/internal/vm/create.go index 243a8d6..49d0dd8 100644 --- a/internal/vm/create.go +++ b/internal/vm/create.go @@ -3,6 +3,7 @@ package vm import ( "fmt" "strconv" + "strings" "git.g3e.fr/syonad/two/internal/iptables" "git.g3e.fr/syonad/two/internal/metadata" @@ -23,16 +24,23 @@ func StartVM(db *badger.DB, name string) error { return nil } - vpcName, err := kv.GetFromDB(db, "vm/"+name+"/vpc") + subnetName, err := kv.GetFromDB(db, "vm/"+name+"/subnet") + if err != nil { + return fmt.Errorf("get subnet: %w", err) + } + + vpcName, err := kv.GetFromDB(db, "subnet/"+subnetName+"/vpc") if err != nil { return fmt.Errorf("get vpc: %w", err) } - bridge, err := kv.GetFromDB(db, "vm/"+name+"/bridge") + gatewayIP, err := kv.GetFromDB(db, "subnet/"+subnetName+"/gateway_ip") if err != nil { - return fmt.Errorf("get bridge: %w", err) + return fmt.Errorf("get gateway_ip: %w", err) } + bridge := "br-" + strings.SplitN(subnetName, "-", 2)[1] + tapIDStr, err := kv.GetFromDB(db, "vm/"+name+"/tap_id") if err != nil { return fmt.Errorf("get tap_id: %w", err) @@ -47,11 +55,6 @@ func StartVM(db *badger.DB, name string) error { return fmt.Errorf("get ip: %w", err) } - gatewayIP, err := kv.GetFromDB(db, "vm/"+name+"/gateway_ip") - if err != nil { - return fmt.Errorf("get gateway_ip: %w", err) - } - metadataPort, err := kv.GetFromDB(db, "vm/"+name+"/metadata_port") if err != nil { return fmt.Errorf("get metadata_port: %w", err) diff --git a/internal/vm/delete.go b/internal/vm/delete.go index 7e89424..0de2b73 100644 --- a/internal/vm/delete.go +++ b/internal/vm/delete.go @@ -25,11 +25,21 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error { return nil } - vpcName, err := kv.GetFromDB(db, "vm/"+name+"/vpc") + subnetName, err := kv.GetFromDB(db, "vm/"+name+"/subnet") + if err != nil { + return fmt.Errorf("get subnet: %w", err) + } + + vpcName, err := kv.GetFromDB(db, "subnet/"+subnetName+"/vpc") if err != nil { return fmt.Errorf("get vpc: %w", err) } + gatewayIP, err := kv.GetFromDB(db, "subnet/"+subnetName+"/gateway_ip") + if err != nil { + return fmt.Errorf("get gateway_ip: %w", err) + } + tapIDStr, err := kv.GetFromDB(db, "vm/"+name+"/tap_id") if err != nil { return fmt.Errorf("get tap_id: %w", err) @@ -44,11 +54,6 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error { return fmt.Errorf("get ip: %w", err) } - gatewayIP, err := kv.GetFromDB(db, "vm/"+name+"/gateway_ip") - if err != nil { - return fmt.Errorf("get gateway_ip: %w", err) - } - metadataPort, err := kv.GetFromDB(db, "vm/"+name+"/metadata_port") if err != nil { return fmt.Errorf("get metadata_port: %w", err) From cc9998028dfd5f7e1c7a3e08a100e9cfb4d60776 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 17:57:16 +0200 Subject: [PATCH 05/12] f-25: code: move load data to dedicated function Signed-off-by: GnomeZworc --- internal/subnet/create.go | 97 +++++++++--------------------------- internal/subnet/data.go | 72 +++++++++++++++++++++++++++ internal/subnet/delete.go | 43 +++++----------- internal/vm/create.go | 78 ++++------------------------- internal/vm/data.go | 100 ++++++++++++++++++++++++++++++++++++++ internal/vm/delete.go | 40 ++------------- 6 files changed, 222 insertions(+), 208 deletions(-) create mode 100644 internal/subnet/data.go create mode 100644 internal/vm/data.go diff --git a/internal/subnet/create.go b/internal/subnet/create.go index 496c257..07658c6 100644 --- a/internal/subnet/create.go +++ b/internal/subnet/create.go @@ -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,32 +76,29 @@ 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 { diff --git a/internal/subnet/data.go b/internal/subnet/data.go new file mode 100644 index 0000000..31f5472 --- /dev/null +++ b/internal/subnet/data.go @@ -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 +} diff --git a/internal/subnet/delete.go b/internal/subnet/delete.go index 8ce91ff..8606cb9 100644 --- a/internal/subnet/delete.go +++ b/internal/subnet/delete.go @@ -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,49 @@ 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) } - // 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) } diff --git a/internal/vm/create.go b/internal/vm/create.go index 49d0dd8..4737e62 100644 --- a/internal/vm/create.go +++ b/internal/vm/create.go @@ -2,8 +2,6 @@ package vm import ( "fmt" - "strconv" - "strings" "git.g3e.fr/syonad/two/internal/iptables" "git.g3e.fr/syonad/two/internal/metadata" @@ -24,84 +22,28 @@ func StartVM(db *badger.DB, name string) error { return nil } - subnetName, err := kv.GetFromDB(db, "vm/"+name+"/subnet") + d, err := loadVM(db, name) if err != nil { - return fmt.Errorf("get subnet: %w", err) + return err } - vpcName, err := kv.GetFromDB(db, "subnet/"+subnetName+"/vpc") - if err != nil { - return fmt.Errorf("get vpc: %w", err) - } - - gatewayIP, err := kv.GetFromDB(db, "subnet/"+subnetName+"/gateway_ip") - if err != nil { - return fmt.Errorf("get gateway_ip: %w", err) - } - - bridge := "br-" + strings.SplitN(subnetName, "-", 2)[1] - - tapIDStr, err := kv.GetFromDB(db, "vm/"+name+"/tap_id") - if err != nil { - return fmt.Errorf("get tap_id: %w", err) - } - tapID, err := strconv.Atoi(tapIDStr) - if err != nil { - return fmt.Errorf("parse tap_id: %w", err) - } - - vmIP, err := kv.GetFromDB(db, "vm/"+name+"/ip") - if err != nil { - return fmt.Errorf("get ip: %w", err) - } - - metadataPort, err := kv.GetFromDB(db, "vm/"+name+"/metadata_port") - if err != nil { - return fmt.Errorf("get metadata_port: %w", err) - } - - mac, err := kv.GetFromDB(db, "vm/"+name+"/mac") - if err != nil { - return fmt.Errorf("get mac: %w", err) - } - - volumePath, err := kv.GetFromDB(db, "vm/"+name+"/volume_path") - if err != nil { - return fmt.Errorf("get volume_path: %w", err) - } - - memoryStr, err := kv.GetFromDB(db, "vm/"+name+"/memory") - if err != nil { - return fmt.Errorf("get memory: %w", err) - } - memory, _ := strconv.Atoi(memoryStr) - - cpusStr, err := kv.GetFromDB(db, "vm/"+name+"/cpus") - if err != nil { - return fmt.Errorf("get cpus: %w", err) - } - cpus, _ := strconv.Atoi(cpusStr) - - password, _ := kv.GetFromDB(db, "vm/"+name+"/password") - sshkey, _ := kv.GetFromDB(db, "vm/"+name+"/sshkey") - - if err := netif.CreateTap(tapID, bridge, vpcName); err != nil { + if err := netif.CreateTap(d.tapID, d.bridge, d.vpcName); err != nil { return fmt.Errorf("create tap: %w", err) } - if err := netns.Call(vpcName, func() error { - return iptables.AddMetadataRedirect(vmIP, gatewayIP, metadataPort) + 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: vpcName, - BindIP: gatewayIP, - BindPort: metadataPort, - Password: password, - SSHKEY: sshkey, + VpcName: d.vpcName, + BindIP: d.gatewayIP, + BindPort: d.metadataPort, + Password: d.password, + SSHKEY: d.sshkey, }, db, false); err != nil { return fmt.Errorf("start metadata: %w", err) } diff --git a/internal/vm/data.go b/internal/vm/data.go new file mode 100644 index 0000000..379ce50 --- /dev/null +++ b/internal/vm/data.go @@ -0,0 +1,100 @@ +package vm + +import ( + "fmt" + "strconv" + "strings" + + "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 { + return d, fmt.Errorf("get tap_id: %w", err) + } + 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 := kv.GetFromDB(db, "vm/"+name+"/mac") + if err != nil { + return d, fmt.Errorf("get mac: %w", 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, _ = strconv.Atoi(memoryStr) + + cpusStr, err := kv.GetFromDB(db, "vm/"+name+"/cpus") + if err != nil { + return d, fmt.Errorf("get cpus: %w", err) + } + d.cpus, _ = strconv.Atoi(cpusStr) + + d.password, _ = kv.GetFromDB(db, "vm/"+name+"/password") + d.sshkey, _ = kv.GetFromDB(db, "vm/"+name+"/sshkey") + + return d, nil +} diff --git a/internal/vm/delete.go b/internal/vm/delete.go index 0de2b73..faadb4b 100644 --- a/internal/vm/delete.go +++ b/internal/vm/delete.go @@ -2,7 +2,6 @@ package vm import ( "fmt" - "strconv" "time" configuration "git.g3e.fr/syonad/two/internal/config/agent" @@ -25,38 +24,9 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error { return nil } - subnetName, err := kv.GetFromDB(db, "vm/"+name+"/subnet") + d, err := loadVM(db, name) if err != nil { - return fmt.Errorf("get subnet: %w", err) - } - - vpcName, err := kv.GetFromDB(db, "subnet/"+subnetName+"/vpc") - if err != nil { - return fmt.Errorf("get vpc: %w", err) - } - - gatewayIP, err := kv.GetFromDB(db, "subnet/"+subnetName+"/gateway_ip") - if err != nil { - return fmt.Errorf("get gateway_ip: %w", err) - } - - tapIDStr, err := kv.GetFromDB(db, "vm/"+name+"/tap_id") - if err != nil { - return fmt.Errorf("get tap_id: %w", err) - } - tapID, err := strconv.Atoi(tapIDStr) - if err != nil { - return fmt.Errorf("parse tap_id: %w", err) - } - - vmIP, err := kv.GetFromDB(db, "vm/"+name+"/ip") - if err != nil { - return fmt.Errorf("get ip: %w", err) - } - - metadataPort, err := kv.GetFromDB(db, "vm/"+name+"/metadata_port") - if err != nil { - return fmt.Errorf("get metadata_port: %w", err) + return err } socketPath := fmt.Sprintf("/tmp/%s.qmp-sock", name) @@ -81,8 +51,8 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error { } } - if err := netns.Call(vpcName, func() error { - return iptables.DeleteMetadataRedirect(vmIP, gatewayIP, metadataPort) + 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) } @@ -91,7 +61,7 @@ func StopVM(db *badger.DB, name string, cfg *configuration.Config) error { return fmt.Errorf("stop metadata: %w", err) } - if err := netif.DeleteTap(tapID, vpcName); err != nil { + if err := netif.DeleteTap(d.tapID, d.vpcName); err != nil { return fmt.Errorf("delete tap: %w", err) } From 371ec7546895bc6af7f5cbc4c4408675dede6c3d Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 18:01:39 +0200 Subject: [PATCH 06/12] f-25: vm: generate tap_id Signed-off-by: GnomeZworc --- internal/vm/data.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/internal/vm/data.go b/internal/vm/data.go index 379ce50..04184bd 100644 --- a/internal/vm/data.go +++ b/internal/vm/data.go @@ -2,6 +2,7 @@ package vm import ( "fmt" + "math/rand" "strconv" "strings" @@ -49,13 +50,17 @@ func loadVM(db *badger.DB, name string) (vmData, error) { tapIDStr, err := kv.GetFromDB(db, "vm/"+name+"/tap_id") if err != nil { - return d, fmt.Errorf("get tap_id: %w", err) + 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 } - 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 { From b4092fbe02b7a98e1c42d8eb6e0d23b883d48836 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 18:08:04 +0200 Subject: [PATCH 07/12] f-25: dhcp: fill db with dhcp data Signed-off-by: GnomeZworc --- internal/dhcp/db.go | 19 +++++++++++++++++++ internal/dhcp/dhcp_test.go | 14 +++++++------- internal/dhcp/generate.go | 12 +++++++----- internal/subnet/create.go | 6 +++++- internal/subnet/delete.go | 3 +++ 5 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 internal/dhcp/db.go diff --git a/internal/dhcp/db.go b/internal/dhcp/db.go new file mode 100644 index 0000000..c93aa1b --- /dev/null +++ b/internal/dhcp/db.go @@ -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) +} diff --git a/internal/dhcp/dhcp_test.go b/internal/dhcp/dhcp_test.go index c4a659f..1681fec 100644 --- a/internal/dhcp/dhcp_test.go +++ b/internal/dhcp/dhcp_test.go @@ -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) { diff --git a/internal/dhcp/generate.go b/internal/dhcp/generate.go index 01bfe20..3c46fe2 100644 --- a/internal/dhcp/generate.go +++ b/internal/dhcp/generate.go @@ -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) { diff --git a/internal/subnet/create.go b/internal/subnet/create.go index 07658c6..5e80634 100644 --- a/internal/subnet/create.go +++ b/internal/subnet/create.go @@ -101,9 +101,13 @@ func CreateSubnet(db *badger.DB, subnetName string) error { 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 { diff --git a/internal/subnet/delete.go b/internal/subnet/delete.go index 8606cb9..e7e3929 100644 --- a/internal/subnet/delete.go +++ b/internal/subnet/delete.go @@ -42,6 +42,9 @@ func DeleteSubnet(db *badger.DB, subnetName string) error { 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) + } if err := ebtables.DeleteARPToGateway(d.bridge, d.gatewayIP.String()); err != nil { return fmt.Errorf("delete ebtables arp rule: %w", err) From f005d9dde7a75896d6053cf715e61a709574f86a Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 18:08:50 +0200 Subject: [PATCH 08/12] f-25: vm: use db data to find mac address Signed-off-by: GnomeZworc --- internal/vm/data.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/vm/data.go b/internal/vm/data.go index 04184bd..dfceb49 100644 --- a/internal/vm/data.go +++ b/internal/vm/data.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "git.g3e.fr/syonad/two/internal/dhcp" "git.g3e.fr/syonad/two/pkg/db/kv" "github.com/dgraph-io/badger/v4" ) @@ -74,9 +75,9 @@ func loadVM(db *badger.DB, name string) (vmData, error) { } d.metadataPort = metadataPort - mac, err := kv.GetFromDB(db, "vm/"+name+"/mac") + mac, err := dhcp.GetMACForIP(db, d.subnetName, d.ip) if err != nil { - return d, fmt.Errorf("get mac: %w", err) + return d, fmt.Errorf("get mac for ip %s: %w", d.ip, err) } d.mac = mac From c17ee79183d3740828710a3c487d12b02cfe7df4 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 18:17:15 +0200 Subject: [PATCH 09/12] f-25: dispatch: add vm dispatcher Signed-off-by: GnomeZworc --- internal/dispatcher/agent/vm_commands.go | 95 ++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 internal/dispatcher/agent/vm_commands.go diff --git a/internal/dispatcher/agent/vm_commands.go b/internal/dispatcher/agent/vm_commands.go new file mode 100644 index 0000000..c0245ca --- /dev/null +++ b/internal/dispatcher/agent/vm_commands.go @@ -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) +} + +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 +} From 1ea3a986b8717a9de5abbcd3d6c69776cfd05d1c Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 20:09:34 +0200 Subject: [PATCH 10/12] f-25: api: add vms routes Signed-off-by: GnomeZworc --- api/agent.yaml | 180 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/api/agent.yaml b/api/agent.yaml index da20853..00d4fd8 100644 --- a/api/agent.yaml +++ b/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: From 593bd42e6a3f5db01dbd0ff42f62eb189108a58f Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 20:50:21 +0200 Subject: [PATCH 11/12] f-25: api: add a new api for vms Signed-off-by: GnomeZworc --- internal/api/agent/models.go | 32 ++++++++++ internal/api/agent/server.go | 2 + internal/api/agent/vm.go | 94 +++++++++++++++++++++++++++++ internal/api/agent/vms.go | 112 +++++++++++++++++++++++++++++++++++ 4 files changed, 240 insertions(+) create mode 100644 internal/api/agent/vm.go create mode 100644 internal/api/agent/vms.go diff --git a/internal/api/agent/models.go b/internal/api/agent/models.go index e826082..f13407b 100644 --- a/internal/api/agent/models.go +++ b/internal/api/agent/models.go @@ -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"` } diff --git a/internal/api/agent/server.go b/internal/api/agent/server.go index 8e3c4e3..4a0fba4 100644 --- a/internal/api/agent/server.go +++ b/internal/api/agent/server.go @@ -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) diff --git a/internal/api/agent/vm.go b/internal/api/agent/vm.go new file mode 100644 index 0000000..a150d16 --- /dev/null +++ b/internal/api/agent/vm.go @@ -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 +} diff --git a/internal/api/agent/vms.go b/internal/api/agent/vms.go new file mode 100644 index 0000000..841c429 --- /dev/null +++ b/internal/api/agent/vms.go @@ -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) +} From 7078691db6a6b41eee891a9b775c5036082e0dc3 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 20:52:25 +0200 Subject: [PATCH 12/12] f-25: bug: fix errors and add verif Signed-off-by: GnomeZworc --- internal/qemu/start_other.go | 2 +- internal/vm/data.go | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/qemu/start_other.go b/internal/qemu/start_other.go index 9201a2d..782a7ee 100644 --- a/internal/qemu/start_other.go +++ b/internal/qemu/start_other.go @@ -9,6 +9,6 @@ type Config struct { TapID, Memory, CPUs int } -func Start(cfg Config) error { +func Start(_ Config) error { return errors.New("vm: not supported on this platform") } diff --git a/internal/vm/data.go b/internal/vm/data.go index dfceb49..0563fc0 100644 --- a/internal/vm/data.go +++ b/internal/vm/data.go @@ -91,13 +91,19 @@ func loadVM(db *badger.DB, name string) (vmData, error) { if err != nil { return d, fmt.Errorf("get memory: %w", err) } - d.memory, _ = strconv.Atoi(memoryStr) + 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, _ = strconv.Atoi(cpusStr) + 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")