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