fix doc with project reality
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
parent
142693879d
commit
9d4d1b38b3
5 changed files with 465 additions and 454 deletions
366
Agent Network.md
366
Agent Network.md
|
|
@ -1,269 +1,157 @@
|
|||
# Le reseaux entre les vms
|
||||
# Réseau entre les VMs
|
||||
|
||||
## Topologie
|
||||
|
||||
```
|
||||
+--------------------------------------------------------------+ +----------------------------------------------------+
|
||||
| HOST A | | HOST B |
|
||||
| | | |
|
||||
| [ netns-1 (VPC 1) ] | | |
|
||||
| | | |
|
||||
| br-subnet-1 (10.0.1.1/32) ---- veth1 <----------------+ | | |
|
||||
| (dans netns) | | |
|
||||
| | | |
|
||||
| br-subnet-2 (10.0.2.1/32) ---- veth2 <----------------+ | | |
|
||||
| (dans netns) | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| [ 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) |
|
||||
+-----------------------+ +-----------------------+
|
||||
|
||||
Root netns
|
||||
├── vp-<vpcID>-e ← veth externe vers le netns VPC
|
||||
├── br-<subnetID> ← bridge subnet (côté root)
|
||||
│ ├── v-<subnetID>-e ← veth externe subnet
|
||||
│ └── vxlan-<vxlanID> ← tunnel inter-host
|
||||
│
|
||||
netns.<vpcName>
|
||||
├── br-public ← bridge d'accès public du VPC
|
||||
│ └── vp-<vpcID>-i ← veth interne VPC
|
||||
├── br-<subnetID> ← bridge subnet (côté VPC, isolé)
|
||||
│ ├── v-<subnetID>-i ← veth interne subnet
|
||||
│ └── tap<ID> ← interface de la VM
|
||||
└── ...
|
||||
```
|
||||
|
||||
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
|
||||
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)
|
||||
}
|
||||
func Create(name string) error // CLONE_NEWNET + bind mount sur /var/run/netns/<name>
|
||||
func Delete(name string) error // unmount + suppression du fichier
|
||||
func Call(name string, fn func() error) error // exécute fn dans le netns, restaure le ns d'origine
|
||||
func Enter(name string) error // entre définitivement dans le netns
|
||||
func Exist(name string) bool
|
||||
```
|
||||
|
||||
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
|
||||
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")
|
||||
func CreateBridge(name string, mtu int) error
|
||||
func BridgeSetMaster(iface, bridge string) error
|
||||
func CreateVethToNetns(rootIf, nsIf, netnsPath string, mtu int) error
|
||||
func CreateVxlan(name string, vxlanID int, localIface string, mtu int) error
|
||||
func CreateTap(tapID int, bridgeName, vpcName string) error
|
||||
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, c’est **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
|
||||
package main
|
||||
|
||||
import (
|
||||
"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)
|
||||
}
|
||||
func GenerateConfig(c Config) (string, map[string]string, error)
|
||||
func StoreDHCPEntries(db *badger.DB, subnetName string, entries map[string]string) error
|
||||
func GetMACForIP(db *badger.DB, subnetName, ip string) (string, error)
|
||||
```
|
||||
|
||||
---
|
||||
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
|
||||
sudo ./mon-binaire-go
|
||||
```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 get github.com/coreos/go-systemd/v22
|
||||
```go
|
||||
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 l’unité (`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
|
||||
package vm
|
||||
L'agent utilise **BadgerDB** (KV store embarqué) pour persister l'état de toutes les ressources.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
Chemin par défaut : `/var/lib/two/data/`
|
||||
|
||||
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
|
||||
}
|
||||
Configurable via YAML :
|
||||
```yaml
|
||||
database:
|
||||
path: "/var/lib/two/data/"
|
||||
```
|
||||
|
||||
```go
|
||||
vm, _ := StartVM(cfg)
|
||||
_ = vm.SaveState("") // "" = default dir
|
||||
vm.StartMonitorLoop()
|
||||
## Package `pkg/db/kv`
|
||||
|
||||
| Fonction | Signature | Comportement |
|
||||
|---|---|---|
|
||||
| `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 :
|
||||
|
||||
```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)
|
||||
}
|
||||
}
|
||||
}
|
||||
### Subnet
|
||||
```
|
||||
subnet/<subnet-name>/state → "creating" | "created" | "deleting" | "deleted"
|
||||
subnet/<subnet-name>/vpc → <vpc-name>
|
||||
subnet/<subnet-name>/vxlan_id → <int>
|
||||
subnet/<subnet-name>/local_iface → <bridge-name>
|
||||
subnet/<subnet-name>/gateway_ip → <IP>
|
||||
subnet/<subnet-name>/cidr → <CIDR>
|
||||
```
|
||||
|
||||
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.
|
||||
|
|
|
|||
218
Event Bus.md
218
Event Bus.md
|
|
@ -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`
|
||||
|
||||
```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 qu’un 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
|
||||
## Vue d'ensemble
|
||||
|
||||
```text
|
||||
[API HTTP] --(Push VMEvent)--> [EventQueue] --(notify chan)--> [EventLoop]
|
||||
|
|
||||
[Pop / Switch exec based on event type]
|
||||
[Handler HTTP]
|
||||
│
|
||||
├── 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"
|
||||
```
|
||||
183
vnc.md
Normal file
183
vnc.md
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
# 📄 1. Résumé architecture (Markdown)
|
||||
|
||||
```markdown
|
||||
# Architecture VNC over WebSocket avec multiplexage Unix socket
|
||||
|
||||
## Vue d’ensemble
|
||||
|
||||
Cette architecture permet d’exposer 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 à l’identifiant 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 l’orchestrateur.
|
||||
|
||||
#### Rôle
|
||||
|
||||
* Terminaison TLS
|
||||
* Routage vers le bon host
|
||||
* Basé sur le `vm-id` dans l’URL
|
||||
|
||||
#### 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue