first
This commit is contained in:
7
go.mod
Normal file
7
go.mod
Normal file
@@ -0,0 +1,7 @@
|
||||
module github.com/example/bench-client
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
687
main.go
Normal file
687
main.go
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
225
prompt_bench_client.md
Normal file
225
prompt_bench_client.md
Normal file
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user