- discovery: ping 探测存活 + ARP 解析 MAC,包级 Devices map,串行扫描 - stress: 脚本生成模式(BuildScript/ParseResults),ToolSet 工具检测,包级 Results map - report: 包级 Printed map,SaveAndPrintDeviceReport 保存+打印 - sshclient: 包级 RunCommand - workflow: 仅持有 config,Start() 固定循环,直接调用各包方法 - config: 移除 ScanConfig.Concurrency(串行扫描无需并发数)
236 lines
5.9 KiB
Go
236 lines
5.9 KiB
Go
package stress
|
||
|
||
import (
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"auto-check/pkg/config"
|
||
"auto-check/pkg/sshclient"
|
||
|
||
"golang.org/x/crypto/ssh"
|
||
)
|
||
|
||
// Results 测试结果表(IP → 报告),包级公开
|
||
var Results = make(map[string]*Report)
|
||
|
||
// ============================
|
||
// 执行器
|
||
// ============================
|
||
|
||
// Runner 压力测试执行器
|
||
type Runner struct {
|
||
cfg Config
|
||
types []TestType
|
||
client *ssh.Client
|
||
tools ToolSet
|
||
metrics []Metric
|
||
}
|
||
|
||
// 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(ip string) *Report {
|
||
report := &Report{IP: ip, StartTime: time.Now()}
|
||
fmt.Printf("\n════════ [%s] 压力测试开始 ════════\n", ip)
|
||
|
||
// 显示可用工具
|
||
fmt.Printf(" 工具: ")
|
||
for name, info := range r.tools.Tools {
|
||
fmt.Printf("%s(%s) ", name, info.Version)
|
||
}
|
||
fmt.Println()
|
||
|
||
// 显示将执行的指标
|
||
fmt.Printf(" 指标: ")
|
||
for _, m := range r.metrics {
|
||
if m.Enabled {
|
||
fmt.Printf("%s ", m.Name)
|
||
}
|
||
}
|
||
fmt.Println()
|
||
|
||
// 生成脚本
|
||
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)
|
||
}
|
||
|
||
// 解析结果
|
||
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
|
||
}
|
||
|
||
// 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 的系统信息字段
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================
|
||
// 业务入口(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 报告状态字符串
|
||
func Status(rpt *Report) string {
|
||
if rpt == nil {
|
||
return StatusUnknown
|
||
}
|
||
if IsPassed(rpt) {
|
||
return StatusPass
|
||
}
|
||
return StatusFail
|
||
}
|
||
|
||
// ParseTypes 逗号分隔字符串 → TestType 列表
|
||
func ParseTypes(s string) []TestType {
|
||
var types []TestType
|
||
for _, t := range strings.Split(s, ",") {
|
||
if t = strings.TrimSpace(t); t != "" {
|
||
types = append(types, TestType(t))
|
||
}
|
||
}
|
||
return types
|
||
}
|