add multiple data and architecture document

Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
GnomeZworc 2025-04-24 00:37:22 +02:00
commit 43f7cc86aa
Signed by: nicolas.boufideline
GPG key ID: 4406BBBF8845D632
5 changed files with 665 additions and 1 deletions

@ -27,4 +27,7 @@ Tout cela serait lancer depuis une api:
## Architecture ## Architecture
- [Reseaux](./agent/Network.md) - [Reseaux](./agent/Network.md)
- [Les vms](./agent/Instance.md)
- [Serveur http](./agent/http.md)
- [Persistance](./agent/Persistance.md)

245
agent/Instance.md Normal file

@ -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 lagent
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/<pid>/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)
}
}
```

@ -31,4 +31,164 @@
| (Tunnel inter-host) | | (Tunnel inter-host) | | (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")
``` ```

98
agent/Persistance.md Normal file

@ -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`

158
agent/http.md Normal file

@ -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))
}
```