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
|
||||||
|
|
||||||
```
|
```
|
||||||
+--------------------------------------------------------------+ +----------------------------------------------------+
|
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, 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
|
```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 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
|
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.
|
||||||
|
|
|
||||||
220
Event Bus.md
220
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`
|
## 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 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
|
|
||||||
|
|
||||||
```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
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