重构 auto-check:状态数据下沉各包,workflow 精简为纯调度

- discovery: ping 探测存活 + ARP 解析 MAC,包级 Devices map,串行扫描
- stress: 脚本生成模式(BuildScript/ParseResults),ToolSet 工具检测,包级 Results map
- report: 包级 Printed map,SaveAndPrintDeviceReport 保存+打印
- sshclient: 包级 RunCommand
- workflow: 仅持有 config,Start() 固定循环,直接调用各包方法
- config: 移除 ScanConfig.Concurrency(串行扫描无需并发数)
This commit is contained in:
张威33321
2026-08-12 17:11:59 +08:00
parent 700ba91883
commit 906bb54aaa
15 changed files with 708 additions and 871 deletions

View File

@@ -2,6 +2,7 @@ package stress
import (
"fmt"
"strconv"
"strings"
"time"
@@ -14,145 +15,212 @@ import (
// Results 测试结果表IP → 报告),包级公开
var Results = make(map[string]*Report)
// Runner 压力测试调度器
// ============================
// 执行器
// ============================
// Runner 压力测试执行器
type Runner struct {
Config Config
Types []TestType
Executor Executor
cfg Config
types []TestType
client *ssh.Client
tools ToolSet
metrics []Metric
}
// NewRunner 创建调度器
func NewRunner(cfg Config, types []TestType, executor Executor) *Runner {
return &Runner{
Config: cfg,
Types: types,
Executor: executor,
}
// NewRunner 创建执行器(检测工具 → 构建指标 → 生成脚本 → 执行)
func NewRunner(cfg Config, types []TestType, client *ssh.Client) *Runner {
tools := DetectTools(client)
metrics := BuildMetrics(types, tools)
return &Runner{cfg: cfg, types: types, client: client, tools: tools, metrics: metrics}
}
// Run 执行压力测试(单台主机
func (r *Runner) Run(client *ssh.Client, ip string) *Report {
// Run 执行压力测试(生成脚本 → 远程执行 → 解析结果
func (r *Runner) Run(ip string) *Report {
report := &Report{IP: ip, StartTime: time.Now()}
fmt.Printf("\n════════ [%s] 压力测试开始 ════════\n", ip)
// 系统信息探测
info := ProbeSystem(client, r.Executor)
fmt.Printf(" 主机: %s | CPU: %s (%s核) | 内存: %s | 内核: %s\n",
info.Hostname, info.CPUModel, info.CPUCores, info.MemTotal, info.KernelVer)
// 显示可用工具
fmt.Printf(" 工具: ")
for name, info := range r.tools.Tools {
fmt.Printf("%s(%s) ", name, info.Version)
}
fmt.Println()
// 启动温度监控
tempMon := NewTempMonitor(client, r.Executor, r.Config.TempLogInt)
tempMon.Start()
// 启动 dmesg 监控
dmesgMon := NewDmesgMonitor(client, r.Executor)
dmesgMon.Start()
// 逐项执行测试
for _, t := range r.Types {
fmt.Printf("\n ── 开始: %s ──\n", t)
var res Result
switch t {
case TestCPU:
res = CPUStress(client, r.Executor, r.Config)
case TestMemory:
res = MemoryStress(client, r.Executor, r.Config)
case TestDiskIO:
res = DiskIOStress(client, r.Executor, r.Config)
case TestMemNative:
res = MemNativeStress(client, r.Executor, r.Config)
case TestFull:
res = FullStress(client, r.Executor, r.Config)
default:
res = Result{Type: t, Status: "skip", Error: fmt.Sprintf("未知测试类型: %s", t)}
// 显示将执行的指标
fmt.Printf(" 指标: ")
for _, m := range r.metrics {
if m.Enabled {
fmt.Printf("%s ", m.Name)
}
}
fmt.Println()
report.AddResult(res)
fmt.Printf(" ── 完成: %s [%s] %s ──\n", t, res.Status, res.Duration.Round(time.Millisecond))
// 生成脚本
script := BuildScript(r.metrics, r.cfg)
// 远程执行脚本(通过 stdin 传入)
fmt.Printf(" [执行] 生成脚本 %d 字节,开始远程执行...\n", len(script))
scriptCmd := fmt.Sprintf("cat <<'AUTOCHECKSCRIPT' > /tmp/auto-check.sh\n%s\nAUTOCHECKSCRIPT\nchmod +x /tmp/auto-check.sh && bash /tmp/auto-check.sh 2>&1", script)
output, err := sshclient.RunCommand(r.client, scriptCmd)
if err != nil {
fmt.Printf(" [执行] 脚本执行出错: %v\n", err)
}
// 停止监控,收集结果
tempLogs := tempMon.Stop()
dmesgErrors := dmesgMon.Stop()
// 添加温度报告
if len(tempLogs) > 0 {
maxTemp := ""
for _, line := range tempLogs {
// 提取温度值
parts := strings.Fields(line)
for _, p := range parts {
if strings.HasSuffix(p, "°C") || strings.HasSuffix(p, "C") {
maxTemp = p
}
}
}
tempSummary := fmt.Sprintf("采样 %d 次", len(tempLogs))
if maxTemp != "" {
tempSummary += ", 最高温度: " + maxTemp
}
status := "pass"
if strings.Contains(strings.ToLower(strings.Join(tempLogs, " ")), "throttl") {
status = "fail"
tempSummary += " [检测到降频!]"
}
report.AddResult(Result{
Type: MonitorTemp,
Status: status,
Output: tempSummary,
Duration: time.Since(report.StartTime),
})
}
// 添加 dmesg 报告
if len(dmesgErrors) > 0 {
report.AddResult(Result{
Type: MonitorDmesg,
Status: "fail",
Output: fmt.Sprintf("检测到 %d 条硬件相关报错:\n%s", len(dmesgErrors), strings.Join(dmesgErrors[:min(10, len(dmesgErrors))], "\n")),
Duration: time.Since(report.StartTime),
})
} else {
report.AddResult(Result{
Type: MonitorDmesg,
Status: "pass",
Output: "无硬件相关内核报错",
Duration: time.Since(report.StartTime),
})
// 解析结果
if output != "" {
r.parseResults(report, output)
}
report.EndTime = time.Now()
report.Duration = report.EndTime.Sub(report.StartTime)
fmt.Printf("\n════════ [%s] 压力测试完成 ════════\n", ip)
fmt.Println(report.ToText())
return report
}
// RunAll 对多台主机执行压力测试
func (r *Runner) RunAll(clients map[string]*ssh.Client) []*Report {
var reports []*Report
for ip, client := range clients {
reports = append(reports, r.Run(client, ip))
// parseResults 解析脚本的结构化输出
func (r *Runner) parseResults(report *Report, output string) {
lines := strings.Split(output, "\n")
var currentTest string
var currentStatus string
var currentDuration string
var currentOutput []string
flush := func() {
if currentTest == "" {
return
}
status := StatusPass
switch currentStatus {
case "fail":
status = StatusFail
case "skip":
status = StatusSkip
case "error":
status = StatusError
}
duration, _ := time.ParseDuration(currentDuration)
report.AddResult(Result{
Type: TestType(currentTest),
Status: status,
Output: strings.Join(currentOutput, "\n"),
Duration: duration,
})
currentTest = ""
currentStatus = ""
currentDuration = ""
currentOutput = nil
}
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "===TEST:") {
flush()
currentTest = strings.TrimSuffix(strings.TrimPrefix(line, "===TEST:"), "===")
continue
}
if strings.HasPrefix(line, "===MONITOR:") {
flush()
currentTest = strings.TrimSuffix(strings.TrimPrefix(line, "===MONITOR:"), "===")
continue
}
if line == "===END===" {
flush()
continue
}
// 解析 key:value
if strings.HasPrefix(line, "status:") {
currentStatus = strings.TrimPrefix(line, "status:")
continue
}
if strings.HasPrefix(line, "duration:") {
durStr := strings.TrimPrefix(line, "duration:")
ms, _ := strconv.ParseInt(strings.TrimSuffix(durStr, "ms"), 10, 64)
currentDuration = fmt.Sprintf("%dms", ms)
continue
}
if strings.HasPrefix(line, "output:") {
currentOutput = append(currentOutput, strings.TrimPrefix(line, "output:"))
continue
}
if strings.HasPrefix(line, "samples:") || strings.HasPrefix(line, "max:") {
currentOutput = append(currentOutput, line)
continue
}
// 普通输出行
if currentTest != "" && line != "" {
currentOutput = append(currentOutput, line)
}
}
flush()
// 从系统信息输出解析主机信息
for _, line := range lines {
if strings.HasPrefix(line, "hostname:") {
// 可扩展:存入 report 的系统信息字段
}
}
return reports
}
// ============================
// 业务入口workflow 调用)
// ============================
// TestDevice 对单台设备执行 SSH 登录 + 压力测试(业务入口)
func TestDevice(ip string, cfg config.Config) *Report {
sshCfg := sshclient.NewConfig(cfg.SSH.User, cfg.SSH.Password, cfg.SSH.KeyFile, cfg.SSH.Port, cfg.Scan.Timeout)
conn, err := sshclient.Connect(ip, sshCfg)
if err != nil {
fmt.Printf(" [测试] %s SSH 连接失败: %v\n", ip, err)
return NewSSHFailReport(ip, err)
}
defer conn.Close()
stressCfg := Config{
Duration: cfg.Stress.Duration,
Threads: cfg.Stress.Threads,
DiskSizeMB: 1024,
TempLogInt: 10 * time.Second,
}
return NewRunner(stressCfg, ParseTypes(cfg.Stress.Types), conn).Run(ip)
}
// NewSSHFailReport 构造 SSH 连接失败报告
func NewSSHFailReport(ip string, err error) *Report {
return &Report{
IP: ip,
StartTime: time.Now(),
Results: []Result{{Type: "ssh", Status: StatusFail, Error: err.Error()}},
Failed: 1,
}
}
// ============================
// 工具函数
// ============================
// IsPassed 报告是否通过
func IsPassed(rpt *Report) bool {
return rpt != nil && rpt.Failed == 0 && rpt.Errors == 0
}
// Status 报告状态字符串pass / fail / untested
// Status 报告状态字符串
func Status(rpt *Report) string {
if rpt == nil {
return "untested"
return StatusUnknown
}
if IsPassed(rpt) {
return "pass"
return StatusPass
}
return "fail"
return StatusFail
}
// ParseTypes 逗号分隔字符串 → TestType 列表
@@ -165,39 +233,3 @@ func ParseTypes(s string) []TestType {
}
return types
}
// TestDevice 对单台设备执行 SSH 登录 + 压力测试(业务入口)
func TestDevice(ip string, cfg config.Config) *Report {
client := sshclient.NewClient(cfg.SSH.User, cfg.SSH.Password, cfg.SSH.KeyFile, cfg.SSH.Port, cfg.Scan.Timeout)
conn, err := client.Connect(ip)
if err != nil {
fmt.Printf(" [测试] %s SSH 连接失败: %v\n", ip, err)
return NewSSHFailReport(ip, err)
}
defer conn.Close()
return NewRunner(
Config{Duration: cfg.Stress.Duration, Threads: cfg.Stress.Threads, DiskSizeMB: 1024, TempLogInt: 10 * time.Second},
ParseTypes(cfg.Stress.Types),
client,
).Run(conn, ip)
}
// NewSSHFailReport 构造 SSH 连接失败报告
func NewSSHFailReport(ip string, err error) *Report {
return &Report{
IP: ip,
StartTime: time.Now(),
Results: []Result{{Type: "ssh", Status: "fail", Error: err.Error()}},
Failed: 1,
}
}
// min 取较小值
func min(a, b int) int {
if a < b {
return a
}
return b
}