fix doc with project reality

Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
GnomeZworc 2026-05-18 19:47:02 +02:00
commit 9d4d1b38b3
Signed by: nicolas.boufideline
GPG key ID: 4406BBBF8845D632
5 changed files with 465 additions and 454 deletions

@ -1,269 +1,157 @@
# Le reseaux entre les vms # Réseau entre les VMs
## Topologie
``` ```
+--------------------------------------------------------------+ +----------------------------------------------------+ Root netns
| HOST A | | HOST B | ├── vp-<vpcID>-e ← veth externe vers le netns VPC
| | | | ├── br-<subnetID> ← bridge subnet (côté root)
| [ netns-1 (VPC 1) ] | | | │ ├── v-<subnetID>-e ← veth externe subnet
| | | | │ └── vxlan-<vxlanID> ← tunnel inter-host
| br-subnet-1 (10.0.1.1/32) ---- veth1 <----------------+ | | |
| (dans netns) | | | netns.<vpcName>
| | | | ├── br-public ← bridge d'accès public du VPC
| br-subnet-2 (10.0.2.1/32) ---- veth2 <----------------+ | | | │ └── vp-<vpcID>-i ← veth interne VPC
| (dans netns) | | | ├── br-<subnetID> ← bridge subnet (côté VPC, isolé)
| | | | │ ├── v-<subnetID>-i ← veth interne subnet
| | | | │ └── tap<ID> ← interface de la VM
| [ netns-2 (VPC 2) ] | | [ netns-2 (VPC 2) ] | └── ...
| | | |
| br-subnet-3 (10.0.3.1/32) ---- veth3 <------+ | | br-subnet-3 (10.0.3.1/32) ---- veth4 <------+ |
| (dans netns) | | | (dans netns) | |
| | | | | |
+-------------------------------------------------+------------+ +-------------------+--------------------------------+
| |
| |
+----------v-----------+ +-----------v----------+
| br-vx-1 (host) | | br-vx-1 (host) |
| (infra bridge) | | (infra bridge) |
+----------------------+ +----------------------+
| |
+-----------v-----------+ +----------v------------+
| vxlan1001 | | vxlan1001 |
| (Tunnel inter-host) | | (Tunnel inter-host) |
+-----------------------+ +-----------------------+
``` ```
Chaque VPC est un **Linux network namespace**. Les subnets sont des bridges isolés dans ce namespace. Le trafic inter-host passe par des tunnels **VXLAN** sur les bridges root. Les VMs attachent via des interfaces **TAP**.
## Packages
| Package | Rôle |
|---|---|
| `internal/netns` | Cycle de vie des network namespaces |
| `internal/netif` | Création/suppression d'interfaces (bridge, veth, vxlan, tap) |
| `internal/subnet` | Orchestration complète de création/suppression d'un subnet |
| `internal/vpc` | Orchestration complète de création/suppression d'un VPC |
| `internal/dhcp` | Génération de config dnsmasq + stockage des entrées DHCP |
| `internal/ebtables` | Règles L2 (DROP ARP/DHCP) sur les bridges |
| `internal/iptables` | Redirection NAT pour le serveur metadata |
| `pkg/systemd` | Start/Stop de services systemd via D-Bus |
## `internal/netns`
```go ```go
package netutils func Create(name string) error // CLONE_NEWNET + bind mount sur /var/run/netns/<name>
func Delete(name string) error // unmount + suppression du fichier
import ( func Call(name string, fn func() error) error // exécute fn dans le netns, restaure le ns d'origine
"fmt" func Enter(name string) error // entre définitivement dans le netns
"net" func Exist(name string) bool
"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)
}
``` ```
La création utilise `unix.Unshare(CLONE_NEWNET)` + bind mount du thread courant (`/proc/self/task/<tid>/ns/net`) — pas un bind mount du namespace du processus.
## `internal/netif`
```go ```go
CreateNetns("netns-1") func CreateBridge(name string, mtu int) error
CreateBridge("br-subnet-1", "/var/run/netns/netns-1") func BridgeSetMaster(iface, bridge string) error
CreateBridge("br-vx-1", "") // root func CreateVethToNetns(rootIf, nsIf, netnsPath string, mtu int) error
LinkBridgeNetns("veth1", "veth1-peer", "/var/run/netns/netns-1", "br-vx-1", "br-subnet-1") func CreateVxlan(name string, vxlanID int, localIface string, mtu int) error
AddIPToBridge("/var/run/netns/netns-1", "br-subnet-1", "10.0.1.1/32") func CreateTap(tapID int, bridgeName, vpcName string) error
AddLinkLocalRoute("/var/run/netns/netns-1") func DeleteTap(tapID int, vpcName string) error
func DeleteLink(name string) error
func AddrAdd(iface string, ip net.IP) error
func RouteAdd(iface string, subnet *net.IPNet) error
func LinkSetUp(name string) error
func LinkSetDown(name string) error
``` ```
# systemd pour dnsmasq et metadata **Conventions de nommage :**
Oui, cest **tout à fait possible** avec la bibliothèque `go-systemd`, en particulier via le **module `dbus`**, qui permet de démarrer, arrêter et interroger l'état des unités `systemd`. | Interface | Nom | Emplacement |
|---|---|---|
| Bridge subnet | `br-<subnetID>` | root + netns VPC |
| Veth subnet externe | `v-<subnetID>-e` | root |
| Veth subnet interne | `v-<subnetID>-i` | netns VPC |
| Veth VPC externe | `vp-<vpcID>-e` | root |
| Veth VPC interne | `vp-<vpcID>-i` | netns VPC |
| VXLAN | `vxlan-<vxlanID>` | root |
| TAP VM | `tap<ID>` | netns VPC |
| Bridge public VPC | `br-public` | netns VPC |
Voici un exemple minimal de code Go pour **démarrer** et **arrêter** une unité comme `dnsmasq@vpc-00003_br-00000.service` : **Paramètres réseau :** MTU 1500 pour les subnets, MTU 9000 pour les VPCs. VXLAN port 4789, learning désactivé.
--- ## Création d'un subnet
### ✅ Exemple de code pour start/stop via `go-systemd`: `internal/subnet.CreateSubnet(db, subnetName)` :
1. Crée la paire veth `v-<subnetID>-{e,i}` vers le netns VPC
2. Crée `br-<subnetID>` (root) et `br-<subnetID>` (netns VPC)
3. Crée `vxlan-<vxlanID>` dans root
4. Attache les interfaces à leurs bridges (`BridgeSetMaster`)
5. Active toutes les interfaces (`LinkSetUp`)
6. Ajoute l'adresse gateway sur `br-<subnetID>` dans le VPC
7. Ajoute une route `scope link` vers le CIDR du subnet sur `br-<subnetID>` dans le VPC
8. Applique les règles ebtables (ARP + DHCP DROP)
9. Génère la config dnsmasq → `/etc/dnsmasq.d/<vpc>_<bridge>.conf`
10. Démarre `dnsmasq@<vpc>_<bridge>.service` via systemd
Suppression : ordre inverse — arrêt dnsmasq, suppression config, suppression règles ebtables, suppression des interfaces.
## Création d'un VPC
`internal/vpc.CreateVPC(db, vpcName)` :
1. Crée le netns `<vpcName>`
2. Crée la paire veth `vp-<vpcID>-{e,i}`
3. Crée `br-public` dans le netns VPC
4. Attache les veths à `br-public`
5. Active les interfaces
Suppression : supprime `vp-<vpcID>-e` (la paire disparaît) puis supprime le netns.
## DHCP (`internal/dhcp`)
dnsmasq est utilisé en mode static : chaque IP du subnet se voit attribuer une entrée `dhcp-host=<mac>,<ip>`.
```go ```go
package main func GenerateConfig(c Config) (string, map[string]string, error)
func StoreDHCPEntries(db *badger.DB, subnetName string, entries map[string]string) error
import ( func GetMACForIP(db *badger.DB, subnetName, ip string) (string, error)
"context"
"fmt"
"log"
"time"
"github.com/coreos/go-systemd/v22/dbus"
)
func main() {
conn, err := dbus.NewSystemdConnectionContext(context.Background())
if err != nil {
log.Fatalf("Erreur de connexion à systemd: %v", err)
}
defer conn.Close()
unitName := "dnsmasq@vpc-00003_br-00000.service"
// Démarrer l'unité
jobID, err := conn.StartUnitContext(context.Background(), unitName, "replace", nil)
if err != nil {
log.Fatalf("Erreur au démarrage de l'unité: %v", err)
}
fmt.Printf("Service démarré (job ID: %s)\n", jobID)
time.Sleep(3 * time.Second) // attendre un peu
// Arrêter l'unité
jobID, err = conn.StopUnitContext(context.Background(), unitName, "replace", nil)
if err != nil {
log.Fatalf("Erreur à l'arrêt de l'unité: %v", err)
}
fmt.Printf("Service arrêté (job ID: %s)\n", jobID)
}
``` ```
--- Config générée dans `/etc/dnsmasq.d/<vpc>_<bridge>.conf`. Entrées stockées en DB sous `subnet/<subnetName>/dhcp/<ip>`.
### 🔐 Privilèges requis : ## Sécurité réseau
* Il faut que votre programme Go soit exécuté avec des **droits suffisants** (souvent `sudo`) pour interagir avec systemd **au niveau système**. ### ebtables (`internal/ebtables`)
Exemple d'exécution : Appliquées sur chaque bridge subnet :
```bash ```go
sudo ./mon-binaire-go func DropARPToGateway(bridge, gatewayIP string) error // DROP ARP Request vers la gateway
func DropDHCP(bridge string) error // DROP DHCP (ports 67:68)
func DeleteARPToGateway(bridge, gatewayIP string) error
func DeleteDHCP(bridge string) error
``` ```
--- Empêche les VMs de résoudre ou d'usurper la gateway, et d'émettre leur propre DHCP.
### 📦 Dépendance : ### iptables (`internal/iptables`)
Ajoutez ce module dans votre `go.mod` : Redirection metadata cloud-init :
```bash ```go
go get github.com/coreos/go-systemd/v22 func AddMetadataRedirect(vmIP, gatewayIP, metadataPort string) error
func DeleteMetadataRedirect(vmIP, gatewayIP, metadataPort string) error
``` ```
--- Redirige `<vmIP> → 169.254.169.254:80` vers `<gatewayIP>:<metadataPort>` via DNAT (table nat, PREROUTING).
Souhaitez-vous aussi vérifier létat de lunité (`Active`, `Failed`, etc.) ou juste la gestion start/stop ? ## systemd (`pkg/systemd`)
```go
func New() (*Manager, error)
func (m *Manager) Start(service string) error
func (m *Manager) Stop(service string) error
func (m *Manager) Status(service string) (*ServiceStatus, error)
```
Utilise D-Bus (`coreos/go-systemd/v22`). Timeout job : 30 secondes. Service type : `"replace"`.
Services gérés : `dnsmasq@<vpc>_<bridge>.service` (un par subnet).

@ -1,98 +1,68 @@
# Persistance des data # Persistance des données
```go L'agent utilise **BadgerDB** (KV store embarqué) pour persister l'état de toutes les ressources.
package vm
import ( Chemin par défaut : `/var/lib/two/data/`
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
)
const defaultStateDir = "/var/lib/cloud-agent/vms" Configurable via YAML :
```yaml
type VMInstance struct { database:
ID string path: "/var/lib/two/data/"
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 ## Package `pkg/db/kv`
vm, _ := StartVM(cfg)
_ = vm.SaveState("") // "" = default dir | Fonction | Signature | Comportement |
vm.StartMonitorLoop() |---|---|---|
| `InitDB` | `InitDB(conf Config, readonly bool) *badger.DB` | Ouvre la base (supporte mode lecture seule) |
| `AddInDB` | `AddInDB(db *badger.DB, key, value string) error` | Écrit ou écrase une clé |
| `GetFromDB` | `GetFromDB(db *badger.DB, key string) (string, error)` | Lit une clé, retourne `badger.ErrKeyNotFound` si absente |
| `DeleteInDB` | `DeleteInDB(db *badger.DB, key string) error` | Supprime récursivement une clé et tous ses sous-préfixes |
| `ListByPrefix` | `ListByPrefix(db *badger.DB, prefix string) (map[string]string, error)` | Liste toutes les clés commençant par un préfixe |
Un serveur HTTP d'inspection est disponible via `kv.NewAdminServer` (`GET /db?prefix=...`).
## Structure des clés
### VPC
```
vpc/<vpc-name>/state → "creating" | "created" | "deleting" | "deleted"
``` ```
#### Restaurer toutes les VMs au démarrage : ### Subnet
```
```go subnet/<subnet-name>/state → "creating" | "created" | "deleting" | "deleted"
func RestoreVMsOnStartup() { subnet/<subnet-name>/vpc → <vpc-name>
vms, err := LoadAllVMs("") subnet/<subnet-name>/vxlan_id → <int>
if err != nil { subnet/<subnet-name>/local_iface → <bridge-name>
log.Fatalf("Failed to load saved VMs: %v", err) subnet/<subnet-name>/gateway_ip → <IP>
} subnet/<subnet-name>/cidr → <CIDR>
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` ### VM
```
vm/<vm-name>/state → "starting" | "started" | "stopping" | "stopped"
vm/<vm-name>/subnet → <subnet-name>
vm/<vm-name>/ip → <IP>
vm/<vm-name>/metadata_port → <port>
vm/<vm-name>/volume_path → <path>
vm/<vm-name>/memory → <int MB>
vm/<vm-name>/cpus → <int>
vm/<vm-name>/tap_id → <int>
vm/<vm-name>/password → <password> (optionnel)
vm/<vm-name>/sshkey → <pubkey> (optionnel)
```
## Cycle de vie d'une VM
**Démarrage** (`StartVMCommand`) :
1. `Prepare` : vérifie que la VM n'existe pas → écrit toutes les clés avec état `"starting"`
2. `Execute` : attend que le subnet soit `"created"` → lance la VM → passe à `"started"`
**Arrêt** (`StopVMCommand`) :
1. `Prepare` : vérifie que la VM existe → passe à `"stopping"`
2. `Execute` : arrête la VM → passe à `"stopped"``DeleteInDB("vm/<name>")` (supprime toutes les sous-clés)
## États transitoires et pannes
Il n'y a pas de restauration automatique au démarrage. Si l'agent s'arrête pendant une opération, la base conserve l'état intermédiaire (ex: `"starting"`, `"creating"`). Une intervention externe (appel API) est nécessaire pour nettoyer ou relancer.

@ -1,129 +1,99 @@
# A simple event queue # Dispatcher & Queue de commandes
## 🧱 Design simple en Go L'agent traite les opérations (création/suppression de VPC, subnet, VM) via un pattern
**Command** + **Dispatcher** + **Queue de workers**.
### 🎛️ Structure `Event` ## Vue d'ensemble
```go
type EventType string
const (
EventStart EventType = "start"
EventStop EventType = "stop"
EventReboot EventType = "reboot"
EventAttach EventType = "attach-disk"
EventDetach EventType = "detach-disk"
)
type VMEvent struct {
Type EventType
VMID string
Params map[string]string // optionnel selon le type
}
```
---
### 📬 EventQueue (thread-safe FIFO + notify)
```go
type EventQueue struct {
queue []VMEvent
lock sync.Mutex
signal chan struct{}
}
func NewEventQueue() *EventQueue {
return &EventQueue{
queue: make([]VMEvent, 0),
signal: make(chan struct{}, 1),
}
}
func (eq *EventQueue) Push(ev VMEvent) {
eq.lock.Lock()
defer eq.lock.Unlock()
eq.queue = append(eq.queue, ev)
select {
case eq.signal <- struct{}{}:
default:
// si déjà notify, pas besoin de spammer
}
}
func (eq *EventQueue) Pop() (VMEvent, bool) {
eq.lock.Lock()
defer eq.lock.Unlock()
if len(eq.queue) == 0 {
return VMEvent{}, false
}
ev := eq.queue[0]
eq.queue = eq.queue[1:]
return ev, true
}
```
---
### 🔁 Boucle d'exécution FIFO
```go
func StartEventLoop(eq *EventQueue, manager *Manager) {
go func() {
for {
<-eq.signal // bloquant jusqu'à ce quun event arrive
for {
ev, ok := eq.Pop()
if !ok {
break
}
log.Printf("⏳ Processing event %s for VM %s", ev.Type, ev.VMID)
switch ev.Type {
case EventStart:
manager.StartVMByID(ev.VMID)
case EventStop:
manager.StopVMByID(ev.VMID)
case EventAttach:
manager.AttachDisk(ev.VMID, ev.Params["path"])
case EventDetach:
manager.DetachDisk(ev.VMID, ev.Params["path"])
default:
log.Printf("⚠️ Unknown event type: %s", ev.Type)
}
}
}
}()
}
```
---
### 🌐 Exposition via HTTP (exemple minimal)
```go
func handleStartVM(w http.ResponseWriter, r *http.Request, eq *EventQueue) {
vmID := r.URL.Query().Get("id")
if vmID == "" {
http.Error(w, "missing id", 400)
return
}
eq.Push(VMEvent{Type: EventStart, VMID: vmID})
fmt.Fprintf(w, "Start request enqueued")
}
```
---
## 🔔 Résumé de la comm' inter-composants
```text ```text
[API HTTP] --(Push VMEvent)--> [EventQueue] --(notify chan)--> [EventLoop] [Handler HTTP]
|
[Pop / Switch exec based on event type] ├── Prepare(cmd) → validation + écriture état initial en DB (synchrone)
``` │ retourne 4xx si la validation échoue
├── Dispatch(cmd) → enfile cmd.Execute() dans la queue (asynchrone)
└── 202 Accepted → retourné immédiatement au client
[Worker goroutine]
└── consomme la queue → exécute cmd.Execute()
```
## Interface `Command`
```go
type Command interface {
Prepare(db *badger.DB, cfg *configuration.Config) error
Execute(db *badger.DB, cfg *configuration.Config) error
}
```
- **`Prepare`** : valide les prérequis, écrit l'état initial en DB (`"creating"`, `"starting"`…)
- **`Execute`** : effectue l'opération réelle (appels système, réseau, QEMU…)
## Queue (`pkg/worker`)
```go
type Task func()
type Queue struct { tasks chan Task }
func New(bufferSize int) *Queue
func (q *Queue) Submit(t Task) // enfile une tâche
func (q *Queue) Start(n int) // lance n goroutines workers
```
La taille du buffer et le nombre de workers sont configurables :
```yaml
worker:
buffer_size: 1000
count: 4
```
## Dispatcher (`internal/dispatcher/agent`)
```go
type Dispatcher struct { /* queue, db, cfg, logger */ }
func (d *Dispatcher) Prepare(cmd Command) error // appelle cmd.Prepare()
func (d *Dispatcher) Dispatch(cmd Command) // enfile cmd.Execute() + log durée
```
## Commandes disponibles
| Commande | Fichier | États écrits |
|---|---|---|
| `CreateVPCCommand` | `vpc_commands.go` | `creating``created` |
| `DeleteVPCCommand` | `vpc_commands.go` | `deleting``deleted` |
| `CreateSubnetCommand` | `subnet_commands.go` | `creating``created` |
| `DeleteSubnetCommand` | `subnet_commands.go` | `deleting``deleted` |
| `StartVMCommand` | `vm_commands.go` | `starting``started` |
| `StopVMCommand` | `vm_commands.go` | `stopping``stopped` |
## Dépendances entre commandes
Certaines commandes attendent via polling que leurs dépendances soient prêtes :
- `StartVMCommand.Execute` attend `subnet/<name>/state == "created"`
- `DeleteVPCCommand.Execute` attend que tous ses subnets soient `"deleted"`
Le timeout est configurable :
```yaml
dispatcher:
timeout_seconds: 120
poll_seconds: 2
```
## Exemple de flux : POST /vpcs
```text
1. Handler parse la requête → crée CreateVPCCommand{Name: "my-vpc"}
2. Prepare : vérifie que "vpc/my-vpc/state" n'existe pas
écrit "vpc/my-vpc/state" = "creating"
3. Dispatch : enfile Execute() dans la queue
4. Handler : retourne 202 {"name": "my-vpc", "state": "creating"}
5. Worker : exécute vpc.CreateVPC(db, "my-vpc")
passe l'état à "created"
```

@ -8,4 +8,4 @@
- [Metadata](./Agent%20Metadata.md) - [Metadata](./Agent%20Metadata.md)
- [Persistance](./Agent%20Persistance.md) - [Persistance](./Agent%20Persistance.md)
- [HTTP](./Agent%20HTTP.md) - [HTTP](./Agent%20HTTP.md)
- [Event BUS](./Event%20BUS.md) - [Event BUS](./Event%20BUS.md)

183
vnc.md Normal file

@ -0,0 +1,183 @@
# 📄 1. Résumé architecture (Markdown)
```markdown
# Architecture VNC over WebSocket avec multiplexage Unix socket
## Vue densemble
Cette architecture permet dexposer des consoles VNC de machines virtuelles QEMU via WebSocket (compatible noVNC), en utilisant des sockets Unix pour renforcer la sécurité et simplifier le réseau.
```
[ Browser (noVNC) ]
↓ HTTPS / WebSocket
[ Caddy (Global, piloté par orchestrateur) ]
[ VNC Gateway (par host) ]
[ Unix socket VNC ]
[ VM QEMU ]
````
---
## Composants
### 1. QEMU (serveur VNC)
Chaque VM expose son interface VNC via un socket Unix :
```bash
qemu-system-x86_64 -vnc unix:/run/vnc/<vm-id>.sock
````
Caractéristiques :
* Aucun port TCP exposé
* Isolation forte par VM
* Accès restreint via permissions filesystem
---
### 2. Organisation des sockets
```
/run/vnc/
├── vm-1.sock
├── vm-2.sock
└── vm-3.sock
```
Le nom du fichier correspond directement à lidentifiant de la VM (`vm-id`).
---
### 3. VNC Gateway (multiplexeur)
Service unique par host.
#### Fonction
* Écoute sur un port TCP unique (ex: `:9000`)
* Accepte des connexions WebSocket
* Route dynamiquement vers le bon socket Unix
#### Routing implicite
```
/vnc/<vm-id> → /run/vnc/<vm-id>.sock
```
#### Flux
```
WebSocket ⇄ Gateway ⇄ Unix socket ⇄ VNC (QEMU)
```
---
### 4. Caddy (reverse proxy global)
Configuré dynamiquement par lorchestrateur.
#### Rôle
* Terminaison TLS
* Routage vers le bon host
* Basé sur le `vm-id` dans lURL
#### Exemple
```
https://example.com/vnc/vm-123
```
→ routé vers :
```
http://host-A:9000
```
---
### 5. Orchestrateur
Responsable du mapping :
```
vm-id → host
```
Fonctions :
* allocation des VMs
* suivi des hosts
* configuration dynamique de Caddy
---
## Flux complet
```
1. Client ouvre noVNC
2. Connexion WebSocket :
wss://example.com/vnc/<vm-id>
3. Caddy :
→ route vers le bon host
4. Gateway :
→ ouvre /run/vnc/<vm-id>.sock
5. Communication VNC établie
```
---
## Sécurité
### Niveau système
* sockets Unix avec permissions restreintes
* aucun port VNC exposé
### Gateway
* validation stricte du `vm-id`
* prévention path traversal
### Edge (Caddy)
* TLS obligatoire
* authentification possible (JWT, session)
---
## Avantages
* **Pas de ports dynamiques**
* **Multiplexage efficace**
* **Isolation forte (Unix sockets)**
* **Scalabilité horizontale**
* **Simplicité réseau**
* **Compatibilité native avec noVNC**
---
## Points de vigilance
* nettoyage des sockets après arrêt VM
* gestion des timeouts WebSocket
* limites système (file descriptors)
* validation stricte des identifiants
---
## Extension possibles
* multi-tenant (`/run/vnc/<tenant>/<vm>.sock`)
* authentification intégrée gateway
* métriques (connexions actives)
* autoscaling des hosts