From 5b15b56f7a5dfaca795666e3a18fa533bd090f70 Mon Sep 17 00:00:00 2001 From: gilles Date: Sun, 11 Jan 2026 16:12:00 +0100 Subject: [PATCH] first --- go.mod | 7 + main.go | 687 +++++++++++++++++++++++++++++++++++++++++ prompt_bench_client.md | 225 ++++++++++++++ readme.md | 0 4 files changed, 919 insertions(+) create mode 100644 go.mod create mode 100644 main.go create mode 100644 prompt_bench_client.md create mode 100644 readme.md diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2a8004a --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module github.com/example/bench-client + +go 1.21 + +require ( + gopkg.in/yaml.v3 v3.0.1 +) \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..234a9fc --- /dev/null +++ b/main.go @@ -0,0 +1,687 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "flag" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + "sync" + "time" + "unicode" + + "gopkg.in/yaml.v3" +) + +// ========================================== +// 1. TYPES & CONFIGURATION +// ========================================== + +// ConfigStructure représente le fichier config.yaml distant +type Config struct { + ConfigVersion int `yaml:"config_version"` + Runtime struct { + MaxTotalRuntime int `yaml:"max_total_runtime_s"` + CommandTimeout int `yaml:"command_timeout_s"` + TempDir string `yaml:"temp_dir"` + } `yaml:"runtime"` + Backend struct { + URL string `yaml:"url"` + } `yaml:"backend"` + Collection struct { + System struct { + Enabled bool `yaml:"enabled"` + Items []string `yaml:"items"` + } `yaml:"system"` + CPU struct { + Enabled bool `yaml:"enabled"` + } `yaml:"cpu"` + RAM struct { + Enabled bool `yaml:"enabled"` + } `yaml:"ram"` + Storage struct { + Enabled bool `yaml:"enabled"` + } `yaml:"storage"` + Network struct { + Enabled bool `yaml:"enabled"` + } `yaml:"network"` + } `yaml:"collection"` + Benchmarks struct { + Enabled bool `yaml:"enabled"` + Weights map[string]float64 `yaml:"weights"` + CPU struct { + Enabled bool `yaml:"enabled"` + Tool string `yaml:"tool"` + Params struct { + CpuMaxPrime int `yaml:"cpu_max_prime"` + } `yaml:"params"` + } `yaml:"cpu_sysbench"` + Memory struct { + Enabled bool `yaml:"enabled"` + Tool string `yaml:"tool"` + Params struct { + TotalSize string `yaml:"total_size"` + } `yaml:"params"` + } `yaml:"memory_sysbench"` + Disk struct { + Enabled bool `yaml:"enabled"` + Tool string `yaml:"tool"` + Safety struct { + MaxRuntime int `yaml:"max_runtime_s"` + } `yaml:"safety"` + } `yaml:"disk_fio"` + Network struct { + Enabled bool `yaml:"enabled"` + Tool string `yaml:"tool"` + Server string `yaml:"server"` + Port int `yaml:"port"` + Params struct { + Duration int `yaml:"duration_s"` + } `yaml:"params"` + } `yaml:"network_iperf3"` + } `yaml:"benchmarks"` +} + +// FinalPayload est la structure JSON envoyée au backend +type FinalPayload struct { + DeviceIdentifier string `json:"device_identifier"` + BenchClientVersion string `json:"bench_client_version"` + Hardware Hardware `json:"hardware"` + Results Results `json:"results"` + RawInfo map[string]string `json:"raw_info,omitempty"` +} + +type Hardware struct { + CPU CPUInfo `json:"cpu"` + RAM RAMInfo `json:"ram"` + Storage []DiskInfo `json:"storage"` + Network []NetInfo `json:"network"` + System SystemInfo `json:"system"` +} + +type Results struct { + CPU CPUResult `json:"cpu"` + Memory MemResult `json:"memory"` + Disk DiskResult `json:"disk"` + Network NetResult `json:"network"` + GlobalScore float64 `json:"global_score"` +} + +type SystemInfo struct { + OS string `json:"os"` + Kernel string `json:"kernel"` + Arch string `json:"architecture"` + Hostname string `json:"hostname"` + Virtualization string `json:"virtualization"` +} + +type CPUInfo struct { + Model string `json:"model"` + Cores int `json:"cores"` + Threads int `json:"threads"` +} + +type RAMInfo struct { + TotalMB int `json:"total_mb"` +} + +type DiskInfo struct { + Name string `json:"name"` + SizeGB string `json:"size_gb"` + Type string `json:"type"` +} + +type NetInfo struct { + Name string `json:"name"` + IP string `json:"ip_address"` + SpeedMbps string `json:"speed_mbps"` +} + +type CPUResult struct { + ScoreSingle float64 `json:"score_single"` + ScoreMulti float64 `json:"score_multi"` +} + +type MemResult struct { + Throughput float64 `json:"throughput_mib_s"` + Score float64 `json:"score"` +} + +type DiskResult struct { + ReadMBs float64 `json:"read_mb_s"` + WriteMBs float64 `json:"write_mb_s"` + Score float64 `json:"score"` +} + +type NetResult struct { + Upload float64 `json:"upload_mbps"` + Download float64 `json:"download_mbps"` + Score float64 `json:"score"` +} + +// ========================================== +// 2. GLOBALS & UTILS +// ========================================== + +var ( + cfg Config + version = "1.0.0" + debug = false + dryRun = false +) + +// safeRun exécute une commande avec timeout et gestion d'erreur +func safeRun(ctx context.Context, name string, args ...string) (string, error) { + if debug { + fmt.Printf("[DEBUG] Exec: %s %v\n", name, args) + } + if dryRun { + return "DRY RUN OUTPUT", nil + } + + cmd := exec.CommandContext(ctx, name, args...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + return "", fmt.Errorf("%w: %s", err, stderr.String()) + } + return stdout.String(), nil +} + +func printProgress(step, total int, name, status string) { + // Simple affichage ANSI compatible + statusColor := "\033[32m" // Vert + if status == "WARN" || status == "SKIPPED" { + statusColor = "\033[33m" // Jaune + } else if strings.Contains(status, "ERROR") { + statusColor = "\033[31m" // Rouge + } + reset := "\033[0m" + fmt.Printf("[%d/%d] %-20s %s%s%s\n", step, total, name, statusColor, status, reset) +} + +// ========================================== +// 3. CONFIG LOADER +// ========================================== + +func loadConfig(url string) error { + var configData []byte + var err error + + // Tentative HTTP + if url != "" { + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Get(url) + if err == nil { + defer resp.Body.Close() + configData, err = io.ReadAll(resp.Body) + if err == nil { + fmt.Println("INFO: Configuration distante chargée.") + goto parse + } + } + fmt.Printf("WARN: Impossible de charger la config distante (%s), tentative fallback...\n", err) + } + + // Fallback local (ou simulation pour cet exemple) + // Dans le réel, on lirait /var/cache/bench-client/config.yaml + fmt.Println("INFO: Utilisation de la configuration embarquée (fallback).") + configData = []byte(defaultConfigYAML) // Utilisation d'une constante pour la démo + +parse: + return yaml.Unmarshal(configData, &cfg) +} + +// ========================================== +// 4. COLLECTORS +// ========================================== + +func collectSystemInfo(ctx context.Context) SystemInfo { + info := SystemInfo{} + + // Hostname + if h, err := os.Hostname(); err == nil { + info.Hostname = h + } + + // OS / Kernel + // Lecture simple de /etc/os-release + if out, err := safeRun(ctx, "sh", "-c", "cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2"); err == nil { + info.OS = strings.Trim(out, "\"\n") + } + if out, err := safeRun(ctx, "uname", "-r"); err == nil { + info.Kernel = strings.TrimSpace(out) + } + if out, err := safeRun(ctx, "uname", "-m"); err == nil { + info.Arch = strings.TrimSpace(out) + } + if out, err := safeRun(ctx, "systemd-detect-virt"); err == nil { + info.Virtualization = strings.TrimSpace(out) + } + + return info +} + +func collectCPU(ctx context.Context) CPUInfo { + cpu := CPUInfo{} + if !cfg.Collection.CPU.Enabled { + return cpu + } + + out, err := safeRun(ctx, "lscpu") + if err != nil { + fmt.Println("WARN: lscpu failed") + return cpu + } + + // Parsing basique via regex + reModel := regexp.MustCompile(`Model name:\s+(.*)`) + reCores := regexp.MustCompile(`^CPU\(s\):\s+(\d+)`) + reThreads := regexp.MustCompile(`Thread\(s\) per core:\s+(\d+)`) + + lines := strings.Split(out, "\n") + for _, line := range lines { + if m := reModel.FindStringSubmatch(line); m != nil { + cpu.Model = strings.TrimSpace(m[1]) + } + if m := reCores.FindStringSubmatch(line); m != nil { + cpu.Cores, _ = strconv.Atoi(m[1]) + } + if m := reThreads.FindStringSubmatch(line); m != nil { + cpu.Threads, _ = strconv.Atoi(m[1]) + } + } + return cpu +} + +func collectRAM(ctx context.Context) RAMInfo { + ram := RAMInfo{} + if !cfg.Collection.RAM.Enabled { + return ram + } + + out, err := safeRun(ctx, "free", "-m") + if err != nil { + return ram + } + + // Parsing de 'free -m' (ligne Mem:) + lines := strings.Split(out, "\n") + if len(lines) > 1 { + fields := strings.Fields(lines[1]) + if len(fields) > 1 { + ram.TotalMB, _ = strconv.Atoi(fields[1]) + } + } + return ram +} + +func collectStorage(ctx context.Context) []DiskInfo { + disks := []DiskInfo{} + if !cfg.Collection.Storage.Enabled { + return disks + } + + out, err := safeRun(ctx, "lsblk", "-d", "-n", "-o", "NAME,SIZE,ROTA") + if err != nil { + return disks + } + + lines := strings.Split(out, "\n") + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) >= 3 { + dType := "ssd" + if parts[2] == "1" { + dType = "hdd" + } + disks = append(disks, DiskInfo{ + Name: parts[0], + SizeGB: parts[1], + Type: dType, + }) + } + } + return disks +} + +func collectNetwork(ctx context.Context) []NetInfo { + nets := []NetInfo{} + if !cfg.Collection.Network.Enabled { + return nets + } + + // Récupérer les interfaces UP + out, err := safeRun(ctx, "ip", "-j", "addr") + if err != nil { + return nets + } + + // ip -j addr renvoie du JSON, facile à parser + var ipData []map[string]interface{} + json.Unmarshal([]byte(out), &ipData) + + for _, iface := range ipData { + ifname, _ := iface["ifname"].(string) + if ifname == "lo" { + continue + } + + operstate, _ := iface["operstate"].(string) + if operstate != "UP" && operstate != "UNKNOWN" { + continue + } + + addrInfo, _ := iface["addr_info"].([]interface{}) + ipStr := "" + if len(addrInfo) > 0 { + firstAddr := addrInfo[0].(map[string]interface{}) + ipStr, _ = firstAddr["local"].(string) + } + + nets = append(nets, NetInfo{ + Name: ifname, + IP: ipStr, + }) + } + return nets +} + +// ========================================== +// 5. BENCHMARKS +// ========================================== + +func runCPUBench(ctx context.Context) CPUResult { + res := CPUResult{} + if !cfg.Benchmarks.CPU.Enabled { + return res + } + + fmt.Println(" -> Single thread...") + // sysbench cpu --threads=1 run + args := []string{"cpu", "--threads=1", "--time=10", "run"} + if out, err := safeRun(ctx, "sysbench", args...); err == nil { + // Parse: events per second: + re := regexp.MustCompile(`events per second:\s+([\d\.]+)`) + if m := re.FindStringSubmatch(out); len(m) > 1 { + val, _ := strconv.ParseFloat(m[1], 64) + res.ScoreSingle = val + } + } + + fmt.Println(" -> Multi thread...") + nproc := os.Getenv("GOMAXPROCS") // fallback + if nproc == "" { + if out, err := safeRun(ctx, "nproc"); err == nil { + nproc = strings.TrimSpace(out) + } else { + nproc = "4" // default safe + } + } + + args = []string{"cpu", fmt.Sprintf("--threads=%s", nproc), "--time=10", "run"} + if out, err := safeRun(ctx, "sysbench", args...); err == nil { + re := regexp.MustCompile(`events per second:\s+([\d\.]+)`) + if m := re.FindStringSubmatch(out); len(m) > 1 { + val, _ := strconv.ParseFloat(m[1], 64) + res.ScoreMulti = val + } + } + return res +} + +func runMemBench(ctx context.Context) MemResult { + res := MemResult{} + if !cfg.Benchmarks.Memory.Enabled { + return res + } + + args := []string{"memory", "--memory-block-size=1K", "--memory-total-size=1G", "run"} + if out, err := safeRun(ctx, "sysbench", args...); err == nil { + // Parse: MiB/sec + re := regexp.MustCompile(`([\d\.]+]\s*MiB/sec`) // exemple: 1024.00 MiB/sec + // Ou plus simple sur la ligne summary: + re2 := regexp.MustCompile(`transferred:\s+([\d\.]+)\s+MiB`) + + if m := re2.FindStringSubmatch(out); len(m) > 1 { + val, _ := strconv.ParseFloat(m[1], 64) + res.Throughput = val + res.Score = val // Score direct + } + } + return res +} + +func runDiskBench(ctx context.Context) DiskResult { + res := DiskResult{} + if !cfg.Benchmarks.Disk.Enabled { + return res + } + + // Utilisation d'un fichier temporaire + tmpFile := filepath.Join(cfg.Runtime.TempDir, "bench.fio") + defer os.Remove(tmpFile) + + args := []string{ + "--name=bench", "--ioengine=libaio", "--rw=randrw", "--bs=4k", + "--direct=1", "--size=500M", "--numjobs=1", "--runtime=30", + "--time_based", "--output-format=json", "--filename=" + tmpFile, + } + + if out, err := safeRun(ctx, "fio", args...); err == nil { + var fioJSON map[string]interface{} + if err := json.Unmarshal([]byte(out), &fioJSON); err == nil { + jobs := fioJSON["jobs"].([]interface{}) + if len(jobs) > 0 { + job := jobs[0].(map[string]interface{}) + read := job["read"].(map[string]interface{}) + write := job["write"].(map[string]interface{}) + + // bw_bytes are usually in bytes/sec + rBw := read["bw"].(float64) / 1024 / 1024 // to MB/s + wBw := write["bw"].(float64) / 1024 / 1024 + + res.ReadMBs = rBw + res.WriteMBs = wBw + res.Score = (rBw + wBw) / 2 + } + } + } + return res +} + +func runNetBench(ctx context.Context) NetResult { + res := NetResult{} + if !cfg.Benchmarks.Network.Enabled { + return res + } + + args := []string{ + "-c", cfg.Benchmarks.Network.Server, + "-p", strconv.Itoa(cfg.Benchmarks.Network.Port), + "-J", "-t", strconv.Itoa(cfg.Benchmarks.Network.Params.Duration), + } + + if out, err := safeRun(ctx, "iperf3", args...); err == nil { + var iperfJSON map[string]interface{} + if err := json.Unmarshal([]byte(out), &iperfJSON); err == nil { + // iperf3 -J structure check + if sum, ok := iperfJSON["end"].(map[string]interface{}); ok { + // Sender = Upload (local sending) + if sumSent, ok := sum["sum_sent"].(map[string]interface{}); ok { + bits := sumSent["bits_per_second"].(float64) + res.Upload = bits / 1000 / 1000 // Mbps + } + // Receiver = Download (local receiving) + if sumRecv, ok := sum["sum_received"].(map[string]interface{}); ok { + bits := sumRecv["bits_per_second"].(float64) + res.Download = bits / 1000 / 1000 + } + res.Score = (res.Upload + res.Download) / 2 + } + } + } + return res +} + +// ========================================== +// 6. MAIN ORCHESTRATOR +// ========================================== + +const defaultConfigYAML = ` +config_version: 1 +runtime: + max_total_runtime_s: 480 + command_timeout_s: 30 + temp_dir: "/tmp" +backend: + url: "http://10.0.0.50:8007/api/benchmark" +collection: + system: { enabled: true } + cpu: { enabled: true } + ram: { enabled: true } + storage: { enabled: true } + network: { enabled: true } +benchmarks: + enabled: true + weights: { cpu: 0.4, memory: 0.2, disk: 0.2, network: 0.1, gpu: 0.1 } + cpu_sysbench: + enabled: true + params: { cpu_max_prime: 20000 } + memory_sysbench: + enabled: true + params: { total_size: "1G" } + disk_fio: + enabled: true + safety: { max_runtime_s: 60 } + network_iperf3: + enabled: true + server: "127.0.0.1" # Default pour test + port: 5201 + params: { duration_s: 10 } +` + +func calculateScore(h Hardware, r Results) float64 { + // Normalisation arbitraire pour l'exemple + // CPU: assume 2000 events/sec is 100 pts + cpuScore := ((r.CPU.ScoreSingle + r.CPU.ScoreMulti) / 2) / 20 + memScore := r.Memory.Throughput / 100 // assume 1000 MB/s is 100 pts + diskScore := r.Disk.Score / 10 // assume 500 MB/s is 50 pts + netScore := r.Network.Score / 10 // assume 1000 Mbps is 100 pts + + w := cfg.Benchmarks.Weights + return (cpuScore*w.CPU + memScore*w.Memory + diskScore*w.Disk + netScore*w.Network) * 100 +} + +func main() { + // Flags + configURL := flag.String("config-url", "", "URL de la config distante") + flag.BoolVar(&debug, "debug", false, "Mode debug") + flag.BoolVar(&dryRun, "dry-run", false, "Simulation d'exécution") + flag.Parse() + + // 1. Chargement Config + fmt.Println("[1/6] Configuration...") + if err := loadConfig(*configURL); err != nil { + fmt.Printf("FATAL: Erreur chargement config: %v\n", err) + os.Exit(1) + } + printProgress(1, 6, "Configuration", "OK") + + // 2. Contexte global + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(cfg.Runtime.MaxTotalRuntime)*time.Second) + defer cancel() + + // 3. Collecte Info + fmt.Println("[2/6] Collecte informations système...") + hw := Hardware{ + System: collectSystemInfo(ctx), + CPU: collectCPU(ctx), + RAM: collectRAM(ctx), + Storage: collectStorage(ctx), + Network: collectNetwork(ctx), + } + printProgress(2, 6, "Collecte", "OK") + + // 4. Benchmarks + fmt.Println("[3/6] Exécution Benchmarks...") + results := Results{ + CPU: runCPUBench(ctx), + Memory: runMemBench(ctx), + Disk: runDiskBench(ctx), + Network: runNetBench(ctx), + } + printProgress(3, 6, "Benchmarks", "OK") + + // 5. Score + fmt.Println("[4/6] Calcul Score...") + results.GlobalScore = calculateScore(hw, results) + printProgress(4, 6, "Score", fmt.Sprintf("%.2f", results.GlobalScore)) + + // 6. Payload & Envoi + fmt.Println("[5/6] Préparation Payload...") + rawInfo := make(map[string]string) + if debug { + rawInfo["debug"] = "active" + } + + payload := FinalPayload{ + DeviceIdentifier: hw.System.Hostname, + BenchClientVersion: version, + Hardware: hw, + Results: results, + RawInfo: rawInfo, + } + + jsonData, err := json.MarshalIndent(payload, "", " ") + if err != nil { + fmt.Printf("FATAL: JSON error: %v\n", err) + os.Exit(1) + } + printProgress(5, 6, "JSON", "OK") + + fmt.Println("[6/6] Envoi Backend...") + if !dryRun { + token := os.Getenv("API_TOKEN") + if token == "" { + fmt.Println("WARN: API_TOKEN manquant. Envoi skipped.") + // Pour debug, on affiche le JSON quand même + fmt.Println(string(jsonData)) + return + } + + req, _ := http.NewRequest("POST", cfg.Backend.URL, bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + client := &http.Client{Timeout: 15 * time.Second} + resp, err := client.Do(req) + if err != nil { + printProgress(6, 6, "Envoi HTTP", "ERROR") + fmt.Printf("Erreur: %v\n", err) + // Sauvegarde locale + os.WriteFile("/tmp/bench_payload_last.json", jsonData, 0644) + } else { + defer resp.Body.Close() + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + printProgress(6, 6, "Envoi HTTP", "OK") + } else { + printProgress(6, 6, "Envoi HTTP", fmt.Sprintf("FAIL %d", resp.StatusCode)) + } + } + } else { + fmt.Println("DRY RUN: Payload non envoyé.") + fmt.Println(string(jsonData)) + } +} \ No newline at end of file diff --git a/prompt_bench_client.md b/prompt_bench_client.md new file mode 100644 index 0000000..13c030d --- /dev/null +++ b/prompt_bench_client.md @@ -0,0 +1,225 @@ +# PROMPT — Bench Client Linux (binaire Go / Rust) + +## 🎯 Objectif + +Concevoir un **programme binaire autonome** nommé `bench-client`, exécuté sur des machines **Linux x86\_64**, destiné à collecter des informations système, exécuter des benchmarks contrôlés, produire un JSON structuré et l’envoyer à un backend HTTP. + +Ce programme **remplace un script Bash complexe** et est piloté par un **fichier de configuration YAML distant**. + +--- + +## 🧠 Rôle attendu + +Tu es un **développeur système senior (Linux / Go ou Rust)**. Tu dois produire un binaire **robuste, lisible, extensible**, orienté production. + +--- + +## 🖥️ Environnement cible + +- OS : Linux (Debian, Ubuntu, Proxmox VE, dérivés) +- Arch : x86\_64 +- Lancement : `sudo ./bench-client` +- Langage : **Go (priorité)** ou **Rust** +- Binaire déjà compilé (aucune compilation sur la machine cible) +- Pas de dépendances runtime externes (hors outils système) + +--- + +## 🧩 Déploiement (hors programme) + +Un script shell externe (déjà existant) : + +1. installe les outils système requis +2. télécharge le binaire +3. télécharge `config.yaml` depuis un serveur HTTP +4. exporte `API_TOKEN` +5. exécute `bench-client` + +Le **binaire ne gère PAS l’installation**. + +--- + +## ⚙️ Configuration distante (OBLIGATOIRE) + +Le programme doit charger un fichier `config.yaml` distant qui pilote : + +- les sections actives/inactives +- les outils à utiliser +- les paramètres des benchmarks +- les limites de sécurité +- les pondérations du score +- l’URL du backend + +### Contraintes + +- Le token **NE DOIT PAS** être dans la config +- Le token est fourni via `API_TOKEN` +- La config distante doit être : + - validée + - bornée (timeouts, tailles max) + - mise en cache localement + - utilisable en fallback si HTTP indisponible + +--- + +## 🏗️ Architecture interne + +### Core + +- parsing des arguments : + - `--config-url` + - `--debug` + - `--dry-run` +- chargement config YAML +- validation des paramètres +- orchestration des collecteurs + +--- + +### Collecteurs (pattern obligatoire) + +Chaque collecteur implémente : + +- `Check()` → outil présent / droits OK +- `Run(ctx)` → exécution avec timeout +- `Parse(output)` → données structurées +- `Raw()` → sortie brute optionnelle + +Les collecteurs doivent être **indépendants**. + +--- + +## 🔧 Outils systèmes à piloter + +### Informations système + +- `lscpu` +- `free` +- `lsblk` +- `ip` +- `dmidecode` (root) +- `lspci` +- `lsusb` +- `ethtool` (root) +- `iw` +- `smartctl` (root) +- `aplay`, `arecord`, `pactl` +- `systemd-detect-virt` +- `pveversion` +- `xrandr`, `xdpyinfo`, `swaymsg` + +### Benchmarks + +- `sysbench cpu` +- `sysbench memory` +- `fio` (non destructif) +- `iperf3 -J` + +Si un outil est absent : + +- afficher WARN +- continuer l’exécution + +--- + +## 📊 Benchmarks + +- CPU : sysbench single + multi +- RAM : sysbench memory +- Disque : fio randrw +- Réseau : iperf3 bidirectionnel JSON +- GPU : placeholder (désactivé par défaut) + +### Pondérations par défaut + +- CPU : 40 % +- RAM : 20 % +- Disque : 20 % +- Réseau : 10 % +- GPU : 10 % + +Tous les benchmarks doivent : + +- avoir un timeout +- être annulables via context +- afficher une progression CLI + +--- + +## 🖨️ Sortie CLI (temps réel) + +Le programme doit afficher : + +- progression globale `[step/total]` +- état : OK / WARN / ERROR +- logs en direct des benchmarks +- couleurs ANSI si TTY +- mode `--debug` : + - commandes exécutées + - sorties brutes + - JSON sauvegardé dans `/tmp` + +Exemple : + +``` +[3/8] CPU : lscpu… OK (12C / 24T) +[6/8] Disk bench : fio… OK (R=520MB/s W=480MB/s) +``` + +--- + +## 📦 JSON final + +Le JSON généré doit contenir : + +- `device_identifier` +- `bench_client_version` +- `hardware` + - cpu + - ram + - gpu + - storage + - network + - audio + - motherboard + - os +- `results` + - cpu + - memory + - disk + - network + - gpu + - global\_score +- `raw_info` (si debug activé) + +Le JSON est : + +- validé +- envoyé via HTTP POST +- header : + ``` + Authorization: Bearer $API_TOKEN + ``` + +--- + +## 🔐 Sécurité + +- aucun benchmark destructif +- timeouts stricts +- limites configurables +- pas de commandes arbitraires +- pas de secrets dans le binaire + +--- + +## 🎯 Résultat attendu + +Un **bench-client binaire** : + +- stable +- lisible +- extensible +- piloté par config distante +- capable de remplacer totalement le script Bash existant + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..e69de29