新增 auto-check 工具:网段扫描+SSH登录+压力测试永久循环工作流

- pkg/discovery: TCP端口探测存活主机,ARP表解析MAC作为设备唯一标识
- pkg/sshclient: SSH连接(密码/密钥认证)与远程命令执行
- pkg/stress: stress-ng/stressapptest 压测(cpu/memory/disk/memnative/full)+温度与dmesg监控
- pkg/report: 检测报告生成(文本/JSON)
- pkg/workflow: 永久循环工作流(间隔可配),MAC唯一标识设备,增量测试+状态变化打印
- pkg/config: YAML分类配置(scan/ssh/stress/report/workflow)
- cmd: cobra入口,仅 --config 指定配置文件
This commit is contained in:
张威33321
2026-08-10 20:30:23 +08:00
parent c6d83a4a94
commit 19f6050147
18 changed files with 2077 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
package stress
import (
"fmt"
"strings"
"time"
"golang.org/x/crypto/ssh"
)
// Runner 压力测试调度器
type Runner struct {
Config Config
Types []TestType
Executor Executor
}
// NewRunner 创建调度器
func NewRunner(cfg Config, types []TestType, executor Executor) *Runner {
return &Runner{
Config: cfg,
Types: types,
Executor: executor,
}
}
// Run 执行压力测试(单台主机)
func (r *Runner) Run(client *ssh.Client, 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)
// 启动温度监控
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)}
}
report.AddResult(res)
fmt.Printf(" ── 完成: %s [%s] %s ──\n", t, res.Status, res.Duration.Round(time.Millisecond))
}
// 停止监控,收集结果
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),
})
}
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))
}
return reports
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

View File

@@ -0,0 +1,168 @@
package stress
import (
"fmt"
"strings"
"time"
"golang.org/x/crypto/ssh"
)
// truncate 截断过长输出
func truncate(s string, maxLen int) string {
s = strings.TrimSpace(s)
if len(s) > maxLen {
return s[:maxLen] + "\n ... (输出已截断)"
}
return s
}
// ============================
// stress-ng 压测
// ============================
// CPUStress stress-ng CPU 压力测试
func CPUStress(client *ssh.Client, ex Executor, cfg Config) Result {
start := time.Now()
fmt.Printf(" [CPU] stress-ng CPU 压力 (%d线程, %v)...\n", cfg.Threads, cfg.Duration)
info, err := EnsureTool(client, ex, "stress-ng", "apt install stress-ng / opkg install stress-ng")
if err != nil {
return Result{Type: TestCPU, Status: "skip", Error: err.Error(), Duration: time.Since(start)}
}
fmt.Printf(" [CPU] 使用 %s (%s)\n", info.Path, info.Version)
cmd := fmt.Sprintf("%s --cpu %d --cpu-method all --timeout %v --metrics-brief --temp-path /tmp 2>&1",
info.Path, cfg.Threads, cfg.Duration)
out, err := exec(client, ex, cmd)
duration := time.Since(start)
if err != nil {
return Result{Type: TestCPU, Status: "error", Output: truncate(out, 600), Error: err.Error(), Duration: duration}
}
return Result{Type: TestCPU, Status: "pass", Output: truncate(out, 600), Duration: duration}
}
// MemoryStress stress-ng 内存压力测试
func MemoryStress(client *ssh.Client, ex Executor, cfg Config) Result {
start := time.Now()
fmt.Printf(" [Memory] stress-ng 内存压力 (%d线程, %v)...\n", cfg.Threads, cfg.Duration)
info, err := EnsureTool(client, ex, "stress-ng", "apt install stress-ng / opkg install stress-ng")
if err != nil {
return Result{Type: TestMemory, Status: "skip", Error: err.Error(), Duration: time.Since(start)}
}
memWorkers := cfg.Threads
if memWorkers > 4 {
memWorkers = 4
}
cmd := fmt.Sprintf("%s --vm %d --vm-bytes 256M --vm-method all --timeout %v --metrics-brief 2>&1",
info.Path, memWorkers, cfg.Duration)
out, err := exec(client, ex, cmd)
duration := time.Since(start)
if err != nil {
return Result{Type: TestMemory, Status: "error", Output: truncate(out, 600), Error: err.Error(), Duration: duration}
}
return Result{Type: TestMemory, Status: "pass", Output: truncate(out, 600), Duration: duration}
}
// DiskIOStress stress-ng 磁盘IO压力测试
func DiskIOStress(client *ssh.Client, ex Executor, cfg Config) Result {
start := time.Now()
fmt.Printf(" [DiskIO] stress-ng 磁盘IO压力 (%v)...\n", cfg.Duration)
info, err := EnsureTool(client, ex, "stress-ng", "apt install stress-ng / opkg install stress-ng")
if err != nil {
return Result{Type: TestDiskIO, Status: "skip", Error: err.Error(), Duration: time.Since(start)}
}
testDir := cfg.DiskDir
if testDir == "" {
testDir = "/tmp/stress-disk-test"
}
_, _ = exec(client, ex, fmt.Sprintf("mkdir -p %s", testDir))
cmd := fmt.Sprintf("%s --iomix 2 --iomix-bytes %dM --timeout %v --metrics-brief 2>&1",
info.Path, cfg.DiskSizeMB, cfg.Duration)
out, err := exec(client, ex, cmd)
duration := time.Since(start)
_, _ = exec(client, ex, fmt.Sprintf("rm -rf %s", testDir))
if err != nil {
return Result{Type: TestDiskIO, Status: "error", Output: truncate(out, 600), Error: err.Error(), Duration: duration}
}
return Result{Type: TestDiskIO, Status: "pass", Output: truncate(out, 600), Duration: duration}
}
// ============================
// stressapptest 内存精压
// ============================
// MemNativeStress stressapptest 内存稳定性精压
func MemNativeStress(client *ssh.Client, ex Executor, cfg Config) Result {
start := time.Now()
fmt.Printf(" [MemNative] stressapptest 内存精压 (%v)...\n", cfg.Duration)
info, err := EnsureTool(client, ex, "stressapptest", "apt install stressapptest / opkg install stressapptest")
if err != nil {
return Result{Type: TestMemNative, Status: "skip", Error: err.Error(), Duration: time.Since(start)}
}
fmt.Printf(" [MemNative] 使用 %s\n", info.Path)
memSize := cfg.MemSizeMB
if memSize <= 0 {
out, _ := exec(client, ex, "free -m 2>/dev/null | awk '/Mem:/{print int($7*0.6)}'")
out = strings.TrimSpace(out)
if out != "" {
fmt.Sscanf(out, "%d", &memSize)
}
if memSize <= 0 {
memSize = 512
}
}
cmd := fmt.Sprintf("%s -s %d -M %d -f 0 -v 2>&1",
info.Path, int(cfg.Duration.Seconds()), memSize)
out, err := exec(client, ex, cmd)
duration := time.Since(start)
output := truncate(out, 800)
if strings.Contains(strings.ToUpper(output), "PASS") {
return Result{Type: TestMemNative, Status: "pass", Output: output, Duration: duration}
}
if strings.Contains(strings.ToUpper(output), "FAIL") {
return Result{Type: TestMemNative, Status: "fail", Output: output, Duration: duration}
}
if err != nil {
return Result{Type: TestMemNative, Status: "error", Output: output, Error: err.Error(), Duration: duration}
}
return Result{Type: TestMemNative, Status: "pass", Output: output, Duration: duration}
}
// ============================
// 综合压测
// ============================
// FullStress 三合一综合压测 (CPU + 内存 + 磁盘IO 同时进行)
func FullStress(client *ssh.Client, ex Executor, cfg Config) Result {
start := time.Now()
fmt.Printf(" [Full] 三合一综合压测 (CPU %d线程 + 内存 + 磁盘IO, %v)...\n", cfg.Threads, cfg.Duration)
info, err := EnsureTool(client, ex, "stress-ng", "apt install stress-ng / opkg install stress-ng")
if err != nil {
return Result{Type: TestFull, Status: "skip", Error: err.Error(), Duration: time.Since(start)}
}
cmd := fmt.Sprintf("%s --cpu %d --vm 2 --vm-bytes 128M --iomix 1 --iomix-bytes 256M --timeout %v --metrics-brief 2>&1",
info.Path, cfg.Threads, cfg.Duration)
out, err := exec(client, ex, cmd)
duration := time.Since(start)
if err != nil {
return Result{Type: TestFull, Status: "error", Output: truncate(out, 600), Error: err.Error(), Duration: duration}
}
return Result{Type: TestFull, Status: "pass", Output: truncate(out, 600), Duration: duration}
}

View File

@@ -0,0 +1,269 @@
package stress
import (
"fmt"
"strings"
"time"
"golang.org/x/crypto/ssh"
)
// ============================
// 远程命令执行接口
// ============================
// Executor 远程命令执行器
type Executor interface {
RunCommand(client *ssh.Client, command string) (string, error)
}
// exec 执行远程命令(带超时)
func exec(client *ssh.Client, executor Executor, cmd string) (string, error) {
return executor.RunCommand(client, cmd)
}
// execf 格式化执行
func execf(client *ssh.Client, executor Executor, format string, args ...interface{}) (string, error) {
return exec(client, executor, fmt.Sprintf(format, args...))
}
// ============================
// 工具检测
// ============================
// ToolInfo 远程工具信息
type ToolInfo struct {
Available bool
Path string
Version string
}
// DetectTool 检测远程工具是否可用
func DetectTool(client *ssh.Client, executor Executor, name string) ToolInfo {
// 检查路径
out, err := execf(client, executor, "which %s 2>/dev/null", name)
path := strings.TrimSpace(out)
if err != nil || path == "" {
return ToolInfo{Available: false}
}
// 获取版本
var version string
switch name {
case "stress-ng":
out, _ = execf(client, executor, "%s --version 2>&1 | head -1", path)
version = strings.TrimSpace(out)
case "stressapptest":
out, _ = execf(client, executor, "%s --help 2>&1 | head -1", path)
version = strings.TrimSpace(out)
case "lm-sensors":
out, _ = execf(client, executor, "%s -v 2>&1 | head -1", path)
version = strings.TrimSpace(out)
}
return ToolInfo{Available: true, Path: path, Version: version}
}
// EnsureTool 检测工具,不可用则返回错误提示
func EnsureTool(client *ssh.Client, executor Executor, name string, installHint string) (ToolInfo, error) {
info := DetectTool(client, executor, name)
if !info.Available {
return info, fmt.Errorf("%s 未安装。安装方式: %s", name, installHint)
}
return info, nil
}
// ============================
// 环境探测
// ============================
// SystemInfo 系统基础信息
type SystemInfo struct {
Hostname string
CPUModel string
CPUCores string
MemTotal string
KernelVer string
DiskInfo string
}
// ProbeSystem 探测远程系统基础信息
func ProbeSystem(client *ssh.Client, executor Executor) SystemInfo {
info := SystemInfo{}
out, _ := exec(client, executor, "hostname 2>/dev/null")
info.Hostname = strings.TrimSpace(out)
out, _ = exec(client, executor, "lscpu 2>/dev/null | grep 'Model name' | sed 's/Model name:\\s*//'")
info.CPUModel = strings.TrimSpace(out)
out, _ = exec(client, executor, "nproc 2>/dev/null")
info.CPUCores = strings.TrimSpace(out)
out, _ = exec(client, executor, "free -h 2>/dev/null | awk '/Mem:/{print $2}'")
info.MemTotal = strings.TrimSpace(out)
out, _ = exec(client, executor, "uname -r 2>/dev/null")
info.KernelVer = strings.TrimSpace(out)
out, _ = exec(client, executor, "lsblk -d -o NAME,SIZE,TYPE 2>/dev/null | head -5")
info.DiskInfo = strings.TrimSpace(out)
return info
}
// ============================
// 温度监控
// ============================
// TempMonitor 温度监控器
type TempMonitor struct {
client *ssh.Client
executor Executor
interval time.Duration
stopCh chan struct{}
logs []string
}
// NewTempMonitor 创建温度监控器
func NewTempMonitor(client *ssh.Client, executor Executor, interval time.Duration) *TempMonitor {
if interval <= 0 {
interval = 10 * time.Second
}
return &TempMonitor{
client: client,
executor: executor,
interval: interval,
stopCh: make(chan struct{}),
}
}
// Start 后台启动温度监控
func (m *TempMonitor) Start() {
go m.loop()
}
// Stop 停止监控并返回所有采样
func (m *TempMonitor) Stop() []string {
close(m.stopCh)
return m.logs
}
func (m *TempMonitor) loop() {
ticker := time.NewTicker(m.interval)
defer ticker.Stop()
// 初始温度
m.sample()
for {
select {
case <-m.stopCh:
return
case <-ticker.C:
m.sample()
}
}
}
func (m *TempMonitor) sample() {
// 尝试 sensors
out, err := execf(m.client, m.executor, "sensors 2>/dev/null | grep -i 'temp\\|core\\|cpu' | head -10")
if err == nil && strings.TrimSpace(out) != "" {
ts := time.Now().Format("15:04:05")
for _, line := range strings.Split(out, "\n") {
line = strings.TrimSpace(line)
if line != "" {
m.logs = append(m.logs, fmt.Sprintf("[%s] %s", ts, line))
}
}
return
}
// 回退: 读 sysfs
out, _ = execf(m.client, m.executor, `cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | while read t; do echo "$((t/1000))°C"; done`)
if strings.TrimSpace(out) != "" {
ts := time.Now().Format("15:04:05")
m.logs = append(m.logs, fmt.Sprintf("[%s] %s", ts, strings.TrimSpace(out)))
}
}
// ============================
// dmesg 监控
// ============================
// DmesgMonitor 内核日志监控
type DmesgMonitor struct {
client *ssh.Client
executor Executor
stopCh chan struct{}
baseline string // 启动时的 dmesg 行数
logs []string
}
// NewDmesgMonitor 创建 dmesg 监控器
func NewDmesgMonitor(client *ssh.Client, executor Executor) *DmesgMonitor {
return &DmesgMonitor{
client: client,
executor: executor,
stopCh: make(chan struct{}),
}
}
// Start 记录基线,后台监控
func (m *DmesgMonitor) Start() {
out, _ := exec(m.client, m.executor, "dmesg 2>/dev/null | wc -l")
m.baseline = strings.TrimSpace(out)
go m.loop()
}
// Stop 停止监控,返回新增的硬件报错
func (m *DmesgMonitor) Stop() []string {
close(m.stopCh)
// 获取新增的 dmesg 日志中的硬件错误
out, _ := execf(m.client, m.executor,
`dmesg --level=err,crit,alert,emerg 2>/dev/null | tail -30`)
if strings.TrimSpace(out) != "" {
m.logs = append(m.logs, strings.Split(out, "\n")...)
}
// 过滤硬件相关关键词
var hwErrors []string
keywords := []string{"error", "fail", "fault", "warn", "critical", "oom", "panic", "hardware", "thermal", "throttl"}
for _, line := range m.logs {
lower := strings.ToLower(line)
for _, kw := range keywords {
if strings.Contains(lower, kw) {
hwErrors = append(hwErrors, strings.TrimSpace(line))
break
}
}
}
return hwErrors
}
func (m *DmesgMonitor) loop() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-m.stopCh:
return
case <-ticker.C:
// 周期性检查是否有新的硬件相关日志(轻量级)
out, _ := execf(m.client, m.executor,
`dmesg --level=err,crit 2>/dev/null | tail -3`)
if strings.TrimSpace(out) != "" {
ts := time.Now().Format("15:04:05")
for _, line := range strings.Split(out, "\n") {
line = strings.TrimSpace(line)
if line != "" {
m.logs = append(m.logs, fmt.Sprintf("[%s] %s", ts, line))
}
}
}
}
}
}

View File

@@ -0,0 +1,139 @@
package stress
import (
"fmt"
"strings"
"time"
)
// ============================
// 测试类型定义
// ============================
// TestType 压力测试类型
type TestType string
const (
// 单项压测
TestCPU TestType = "cpu" // stress-ng CPU 压力
TestMemory TestType = "memory" // stress-ng 内存压力
TestDiskIO TestType = "disk" // stress-ng 磁盘IO压力
TestMemNative TestType = "memnative" // stressapptest 内存精压
// 综合压测
TestFull TestType = "full" // CPU+内存+磁盘IO 三合一
// 监控(自动附加)
MonitorTemp TestType = "temp" // lm-sensors 温度监控
MonitorDmesg TestType = "dmesg" // dmesg 内核报错监控
)
// ============================
// 配置
// ============================
// Config 压力测试配置
type Config struct {
Duration time.Duration // 总持续时间
Threads int // CPU/内存线程数
MemSizeMB int // 内存测试大小 (MB)0=自动(可用60%)
DiskSizeMB int // 磁盘测试大小 (MB)0=默认1024
DiskDir string // 磁盘测试目录,空=自动临时目录
TempLogInt time.Duration // 温度采样间隔0=10s
}
// DefaultConfig 默认配置
func DefaultConfig() Config {
return Config{
Duration: 30 * time.Second,
Threads: 4,
MemSizeMB: 0, // 自动
DiskSizeMB: 1024,
DiskDir: "",
TempLogInt: 10 * time.Second,
}
}
// ============================
// 单项测试结果
// ============================
// Result 单项测试结果
type Result struct {
Type TestType
Status string // "pass" / "fail" / "skip" / "error"
Output string // 工具输出摘要
Error string // 错误信息
Duration time.Duration
}
// ============================
// 完整测试报告
// ============================
// Report 一次完整压测的报告
type Report struct {
IP string
StartTime time.Time
EndTime time.Time
Duration time.Duration
Results []Result
Passed int
Failed int
Skipped int
Errors int
}
// AddResult 添加测试结果
func (r *Report) AddResult(res Result) {
r.Results = append(r.Results, res)
switch res.Status {
case "pass":
r.Passed++
case "fail":
r.Failed++
case "skip":
r.Skipped++
case "error":
r.Errors++
}
}
// Summary 汇总信息
func (r *Report) Summary() string {
return fmt.Sprintf("共 %d 项: %d 通过, %d 失败, %d 跳过, %d 错误",
len(r.Results), r.Passed, r.Failed, r.Skipped, r.Errors)
}
// ToText 文本格式报告
func (r *Report) ToText() string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("══ [%s] 压力测试报告 ══\n", r.IP))
sb.WriteString(fmt.Sprintf(" 耗时: %s\n\n", r.Duration.Round(time.Millisecond)))
for _, res := range r.Results {
icon := "✓"
switch res.Status {
case "fail":
icon = "✗"
case "skip":
icon = "⊘"
case "error":
icon = "⚠"
}
sb.WriteString(fmt.Sprintf(" %s %-12s %s\n", icon, res.Type, res.Status))
if res.Output != "" {
for _, line := range strings.Split(res.Output, "\n") {
if strings.TrimSpace(line) != "" {
sb.WriteString(fmt.Sprintf(" %s\n", line))
}
}
}
if res.Error != "" {
sb.WriteString(fmt.Sprintf(" 错误: %s\n", res.Error))
}
}
sb.WriteString(fmt.Sprintf("\n %s\n", r.Summary()))
return sb.String()
}