From 43f7cc86aa3c70189787c8f43584501eaace97a4 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Thu, 24 Apr 2025 00:37:22 +0200 Subject: [PATCH] add multiple data and architecture document Signed-off-by: GnomeZworc --- agent/Home.md | 5 +- agent/Instance.md | 245 +++++++++++++++++++++++++++++++++++++++++++ agent/Network.md | 160 ++++++++++++++++++++++++++++ agent/Persistance.md | 98 +++++++++++++++++ agent/http.md | 158 ++++++++++++++++++++++++++++ 5 files changed, 665 insertions(+), 1 deletion(-) create mode 100644 agent/Instance.md create mode 100644 agent/Persistance.md create mode 100644 agent/http.md diff --git a/agent/Home.md b/agent/Home.md index 41cb67b..2ff7ddd 100644 --- a/agent/Home.md +++ b/agent/Home.md @@ -27,4 +27,7 @@ Tout cela serait lancer depuis une api: ## Architecture -- [Reseaux](./agent/Network.md) \ No newline at end of file +- [Reseaux](./agent/Network.md) +- [Les vms](./agent/Instance.md) +- [Serveur http](./agent/http.md) +- [Persistance](./agent/Persistance.md) \ No newline at end of file diff --git a/agent/Instance.md b/agent/Instance.md new file mode 100644 index 0000000..50d30a1 --- /dev/null +++ b/agent/Instance.md @@ -0,0 +1,245 @@ +# Fonctionnement des vms + +## Demarrage + +```go +package vm + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" +) + +type VMConfig struct { + Name string + ImagePath string + TapIfName string + NetNS string + MemoryMB int + CPUs int + QMPSocket string + HMPSocket string +} + +type VMInstance struct { + Pid int + QMPSocket string +} + +func StartVM(cfg VMConfig) (*VMInstance, error) { + qmpSock := cfg.QMPSocket + if qmpSock == "" { + qmpSock = fmt.Sprintf("/run/vms/%s.qmp", cfg.Name) + } + hmpSock := cfg.HMPSocket + if hmpSock == "" { + hmpSock = fmt.Sprintf("/run/vms/%s.sock", cfg.Name) + } + + // Ensure socket dir exists + if err := os.MkdirAll(filepath.Dir(qmpSock), 0755); err != nil { + return nil, fmt.Errorf("failed to create qmp dir: %w", err) + } + + args := []string{ + "-name", cfg.Name, + "-m", fmt.Sprintf("%d", cfg.MemoryMB), + "-smp", fmt.Sprintf("%d", cfg.CPUs), + "-drive", fmt.Sprintf("file=%s,format=qcow2,if=virtio", cfg.ImagePath), + "-netdev", fmt.Sprintf("tap,id=net0,ifname=%s,script=no,downscript=no", cfg.TapIfName), + "-device", "virtio-net-pci,netdev=net0", + "-qmp", fmt.Sprintf("unix:%s,server,nowait", qmpSock), + "-monitor", fmt.Sprintf("unix:%s,server,nowait", hmpSockt), + "-nographic", + } + + cmd := exec.Command("ip", append([]string{"netns", "exec", cfg.NetNS, "qemu-system-x86_64"}, args...)...) + + // Rediriger les logs vers le stdout/stderr de l’agent + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Start() + if err != nil { + return nil, fmt.Errorf("failed to start qemu: %w", err) + } + + return &VMInstance{ + Pid: cmd.Process.Pid, + QMPSocket: qmpSock, + }, nil +} +``` + +## Arret + +- `ShutdownVM(vmID string)` + → envoie `system_powerdown` (soft shutdown) +- `ForceStopVM(vmID string)` + → envoie `quit` (kill direct) + +pour le check du status d'une VM + +```go +// vm/monitor.go +package vm + +import ( + "encoding/json" + "fmt" + "log" + "net" + "os" + "strings" + "time" +) + +func (vm *VMInstance) StartMonitorLoop() { + go func() { + for { + time.Sleep(3 * time.Second) + + // 🔍 Check si le process est zombie + state, err := readProcState(vm.Pid) + if err != nil { + log.Printf("❌ VM %s: impossible de lire /proc: %v", vm.ID, err) + break + } + if state == "Z" { + log.Printf("💀 VM %s (PID %d) est en état zombie", vm.ID, vm.Pid) + break + } + + // 🧠 Ping QMP socket + status, err := queryQMPStatus(vm.QMPSocket) + if err != nil { + log.Printf("⚠️ VM %s: QMP unreachable: %v", vm.ID, err) + } else { + log.Printf("🟢 VM %s status: %s", vm.ID, status) + } + } + }() +} + +// 📄 Lecture du statut dans /proc//stat +func readProcState(pid int) (string, error) { + data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)) + if err != nil { + return "", err + } + parts := strings.Split(string(data), " ") + if len(parts) < 3 { + return "", fmt.Errorf("invalid stat format") + } + return parts[2], nil // 3ème champ = state: R, S, D, Z... +} + +// 📡 Envoie "query-status" sur le QMP +func queryQMPStatus(socketPath string) (string, error) { + conn, err := net.Dial("unix", socketPath) + if err != nil { + return "", err + } + defer conn.Close() + + // Lire la bannière QMP + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil { + return "", err + } + if !strings.Contains(string(buf[:n]), `"QMP"`) { + return "", fmt.Errorf("invalid QMP banner") + } + + // Envoyer query-status + cmd := `{"execute": "query-status"}` + "\n" + if _, err := conn.Write([]byte(cmd)); err != nil { + return "", err + } + + // Lire la réponse + n, err = conn.Read(buf) + if err != nil { + return "", err + } + + var resp map[string]interface{} + if err := json.Unmarshal(buf[:n], &resp); err != nil { + return "", err + } + + if returnVal, ok := resp["return"].(map[string]interface{}); ok { + if status, ok := returnVal["status"].(string); ok { + return status, nil + } + } + return "", fmt.Errorf("unable to parse QMP status") +} +``` + +## check des zombies + +```go +package vm + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" +) + +func DetectZombieVMs(knownVMs map[int]bool) ([]int, error) { + entries, err := os.ReadDir("/proc") + if err != nil { + return nil, err + } + + var zombies []int + + for _, e := range entries { + if !e.IsDir() { + continue + } + pid, err := strconv.Atoi(e.Name()) + if err != nil { + continue + } + + cmdlinePath := filepath.Join("/proc", e.Name(), "cmdline") + data, err := os.ReadFile(cmdlinePath) + if err != nil { + continue + } + + if strings.Contains(string(data), "qemu-system") { + if !knownVMs[pid] { + zombies = append(zombies, pid) + } + } + } + + return zombies, nil +} +``` + +```go +func CheckForZombies(vmList []*VMInstance) { + known := make(map[int]bool) + for _, vm := range vmList { + known[vm.Pid] = true + } + zombies, err := DetectZombieVMs(known) + if err != nil { + log.Printf("Erreur check zombies: %v", err) + return + } + for _, pid := range zombies { + log.Printf("🧟 VM zombie détectée: PID %d (non gérée par l'agent)", pid) + } +} +``` \ No newline at end of file diff --git a/agent/Network.md b/agent/Network.md index 34a316f..24072e2 100644 --- a/agent/Network.md +++ b/agent/Network.md @@ -31,4 +31,164 @@ | (Tunnel inter-host) | | (Tunnel inter-host) | +-----------------------+ +-----------------------+ +``` + +```go +package netutils + +import ( + "fmt" + "net" + + "github.com/vishvananda/netlink" + "github.com/vishvananda/netns" + "golang.org/x/sys/unix" +) + +// CreateNetns crée un nouveau namespace nommé +func CreateNetns(name string) error { + path := fmt.Sprintf("/var/run/netns/%s", name) + return unix.Mount("/proc/self/ns/net", path, "none", unix.MS_BIND, "") +} + +// CreateBridge crée un bridge dans le namespace donné (ou root si nsPath == "") +func CreateBridge(name, nsPath string) error { + link := &netlink.Bridge{ + LinkAttrs: netlink.LinkAttrs{Name: name}, + } + if nsPath != "" { + newNs, err := netns.GetFromPath(nsPath) + if err != nil { + return err + } + defer newNs.Close() + + return netns.Do(newNs, func(_ netns.NsHandle) error { + return netlink.LinkAdd(link) + }) + } + return netlink.LinkAdd(link) +} + +// LinkBridgeNetns connecte deux bridges via un veth pair (br1 dans root, br2 dans netns) +func LinkBridgeNetns(vethName, peerName, netnsPath, bridgeRoot, bridgeInNs string) error { + peer := netlink.Veth{ + LinkAttrs: netlink.LinkAttrs{Name: vethName}, + PeerName: peerName, + } + + if err := netlink.LinkAdd(&peer); err != nil { + return err + } + + // Set peer into target netns + ns, err := netns.GetFromPath(netnsPath) + if err != nil { + return err + } + defer ns.Close() + + linkPeer, err := netlink.LinkByName(peerName) + if err != nil { + return err + } + if err := netlink.LinkSetNsFd(linkPeer, int(ns)); err != nil { + return err + } + + // Attach veth to host bridge + br, err := netlink.LinkByName(bridgeRoot) + if err != nil { + return err + } + veth, _ := netlink.LinkByName(vethName) + if err := netlink.LinkSetMaster(veth, br); err != nil { + return err + } + if err := netlink.LinkSetUp(veth); err != nil { + return err + } + + // Attach peer to bridge in netns + return netns.Do(ns, func(_ netns.NsHandle) error { + peerLink, err := netlink.LinkByName(peerName) + if err != nil { + return err + } + brInNs, err := netlink.LinkByName(bridgeInNs) + if err != nil { + return err + } + if err := netlink.LinkSetMaster(peerLink, brInNs); err != nil { + return err + } + return netlink.LinkSetUp(peerLink) + }) +} + +// AddIPToBridge ajoute une IP en /32 à un bridge dans un namespace +func AddIPToBridge(nsPath, bridgeName, ipCidr string) error { + ns, err := netns.GetFromPath(nsPath) + if err != nil { + return err + } + defer ns.Close() + + return netns.Do(ns, func(_ netns.NsHandle) error { + br, err := netlink.LinkByName(bridgeName) + if err != nil { + return err + } + addr, err := netlink.ParseAddr(ipCidr) + if err != nil { + return err + } + return netlink.AddrAdd(br, addr) + }) +} + +// AddLinkLocalRoute ajoute une route link-local dans un netns +func AddLinkLocalRoute(nsPath string) error { + ns, err := netns.GetFromPath(nsPath) + if err != nil { + return err + } + defer ns.Close() + + return netns.Do(ns, func(_ netns.NsHandle) error { + _, dst, _ := net.ParseCIDR("fe80::/64") + route := &netlink.Route{ + Dst: dst, + Scope: netlink.SCOPE_LINK, + } + return netlink.RouteAdd(route) + }) +} + +// CleanupNetns démonte et supprime un netns (utilise iproute2 fs) +func CleanupNetns(name string) error { + path := fmt.Sprintf("/var/run/netns/%s", name) + if err := unix.Unmount(path, 0); err != nil { + return err + } + return unix.Unlink(path) +} + +// DeleteLink supprime un lien réseau (bridge, veth, etc.) +func DeleteLink(name string) error { + link, err := netlink.LinkByName(name) + if err != nil { + return err + } + return netlink.LinkDel(link) +} +``` + +```go +CreateNetns("netns-1") +CreateBridge("br-subnet-1", "/var/run/netns/netns-1") +CreateBridge("br-vx-1", "") // root +LinkBridgeNetns("veth1", "veth1-peer", "/var/run/netns/netns-1", "br-vx-1", "br-subnet-1") +AddIPToBridge("/var/run/netns/netns-1", "br-subnet-1", "10.0.1.1/32") +AddLinkLocalRoute("/var/run/netns/netns-1") ``` \ No newline at end of file diff --git a/agent/Persistance.md b/agent/Persistance.md new file mode 100644 index 0000000..e5c3c36 --- /dev/null +++ b/agent/Persistance.md @@ -0,0 +1,98 @@ +# Persistance des data + +```go +package vm + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" +) + +const defaultStateDir = "/var/lib/cloud-agent/vms" + +type VMInstance struct { + ID string + Pid int + QMPSocket string + HMPSocket string +} + +func (vm *VMInstance) SaveState(stateDir string) error { + if stateDir == "" { + stateDir = defaultStateDir + } + if err := os.MkdirAll(stateDir, 0755); err != nil { + return err + } + + file := filepath.Join(stateDir, vm.ID+".json") + data, err := json.MarshalIndent(vm, "", " ") + if err != nil { + return err + } + return os.WriteFile(file, data, 0644) +} + +func LoadVMState(path string) (*VMInstance, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var vm VMInstance + if err := json.Unmarshal(data, &vm); err != nil { + return nil, err + } + return &vm, nil +} + +func LoadAllVMs(stateDir string) ([]*VMInstance, error) { + if stateDir == "" { + stateDir = defaultStateDir + } + files, err := filepath.Glob(filepath.Join(stateDir, "*.json")) + if err != nil { + return nil, err + } + + var vms []*VMInstance + for _, file := range files { + vm, err := LoadVMState(file) + if err != nil { + fmt.Printf("⚠️ Erreur chargement VM %s: %v\n", file, err) + continue + } + vms = append(vms, vm) + } + return vms, nil +} +``` + +```go +vm, _ := StartVM(cfg) +_ = vm.SaveState("") // "" = default dir +vm.StartMonitorLoop() +``` + +#### Restaurer toutes les VMs au démarrage : + +```go +func RestoreVMsOnStartup() { + vms, err := LoadAllVMs("") + if err != nil { + log.Fatalf("Failed to load saved VMs: %v", err) + } + for _, vm := range vms { + if vm.IsAlive() { + vm.StartMonitorLoop() + log.Printf("✅ Reattached to running VM: %s (PID %d)", vm.ID, vm.Pid) + } else { + log.Printf("⚠️ VM %s is dead, needs cleanup or recovery", vm.ID) + } + } +} +``` + +il va manquer une fonction `RemoveState(vmID)` après un `CleanupVM` \ No newline at end of file diff --git a/agent/http.md b/agent/http.md new file mode 100644 index 0000000..a19076f --- /dev/null +++ b/agent/http.md @@ -0,0 +1,158 @@ +# Async http server + +```go +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "time" +) + +// ----------- Types JSON-RPC ----------- + +type JSONRPCRequest struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params json.RawMessage `json:"params"` + ID interface{} `json:"id"` +} + +type JSONRPCResponse struct { + JSONRPC string `json:"jsonrpc"` + Result interface{} `json:"result,omitempty"` + Error interface{} `json:"error,omitempty"` + ID interface{} `json:"id"` +} + +type Task struct { + Method string + Params json.RawMessage + ID interface{} +} + +// ----------- Routine d'exécution ----------- + +func execRoutine(taskQueue <-chan Task, resultChan chan<- JSONRPCResponse) { + for task := range taskQueue { + var result interface{} + var err error + + switch task.Method { + case "StartVM": + result, err = handleStartVM(task.Params) + case "StopVM": + result, err = handleStopVM(task.Params) + case "AttachDisk": + result, err = handleAttachDisk(task.Params) + case "RestoreVMs": + result, err = handleRestoreVMs() + default: + err = fmt.Errorf("méthode inconnue : %s", task.Method) + } + + res := JSONRPCResponse{ + JSONRPC: "2.0", + ID: task.ID, + } + + if err != nil { + res.Error = map[string]interface{}{ + "code": -32601, + "message": err.Error(), + } + } else { + res.Result = result + } + + resultChan <- res + } +} + +// ----------- Fonctions simulées ----------- + +func handleStartVM(params json.RawMessage) (string, error) { + var args []string + if err := json.Unmarshal(params, &args); err != nil { + return "", err + } + log.Println("→ StartVM", args) + return "VM démarrée : " + fmt.Sprint(args), nil +} + +func handleStopVM(params json.RawMessage) (string, error) { + var args []string + if err := json.Unmarshal(params, &args); err != nil { + return "", err + } + log.Println("→ StopVM", args) + return "VM arrêtée : " + fmt.Sprint(args), nil +} + +func handleAttachDisk(params json.RawMessage) (string, error) { + var args []string + if err := json.Unmarshal(params, &args); err != nil { + return "", err + } + log.Println("→ AttachDisk", args) + return "Disque attaché : " + fmt.Sprint(args), nil +} + +func handleRestoreVMs() (string, error) { + log.Println("→ RestoreVMs") + return "VMs restaurées", nil +} + +// ----------- Serveur JSON-RPC ----------- + +func rpcHandler(taskQueue chan<- Task, resultChan <-chan JSONRPCResponse) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Erreur lecture corps", http.StatusBadRequest) + return + } + + var req JSONRPCRequest + if err := json.Unmarshal(body, &req); err != nil { + http.Error(w, "JSON invalide", http.StatusBadRequest) + return + } + + taskQueue <- Task{ + Method: req.Method, + Params: req.Params, + ID: req.ID, + } + + res := <-resultChan + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(res) + } +} + +// ----------- Main ----------- + +func main() { + log.Println("== Agent Go JSON-RPC ==") + + taskQueue := make(chan Task, 100) + resultChan := make(chan JSONRPCResponse, 100) + + go execRoutine(taskQueue, resultChan) + + http.HandleFunc("/rpc", rpcHandler(taskQueue, resultChan)) + + log.Println("Serveur JSON-RPC sur :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} +``` \ No newline at end of file