Files
membank/auto-check/pkg/workflow/loop.go
张威33321 19f6050147 新增 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 指定配置文件
2026-08-10 20:30:23 +08:00

280 lines
6.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package workflow
import (
"fmt"
"strings"
"time"
"auto-check/pkg/discovery"
"auto-check/pkg/report"
"auto-check/pkg/sshclient"
"auto-check/pkg/stress"
)
// ============================
// 状态常量
// ============================
const (
StatusUntested = "untested" // 未测试
StatusPass = "pass" // 通过
StatusFail = "fail" // 失败
)
// ============================
// 设备MAC 为唯一标识)
// ============================
// Device 设备信息
type Device struct {
MAC string
IP string
Host discovery.Host
}
// ============================
// 永久循环工作流
// ============================
// Loop 永久循环:每间隔执行一轮
// 每轮: 扫描设备 → 增量测试 → 打印状态变化
func Loop(ctx *Context) error {
interval := ctx.Config.Workflow.Interval
if interval <= 0 {
interval = 10 * time.Second
}
fmt.Printf("══ 工作流启动(永久循环,间隔 %v设备以 MAC 唯一标识)══\n\n", interval)
// 第一轮立即执行
round(ctx)
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
fmt.Println("══ 工作流已停止 ══")
return nil
case <-ticker.C:
round(ctx)
}
}
}
// round 执行一轮
func round(ctx *Context) {
now := time.Now().Format("15:04:05")
fmt.Printf("════ 轮次开始 [%s] ════\n", now)
// ── 1. 扫描设备,更新设备列表 mapMAC 为 key──
scanDevices(ctx)
// ── 2. 遍历设备: 结果为空或失败 → 开始测试,保存报告和结果 ──
testPending(ctx)
// ── 3. 遍历打印: 状态变化才重新打印 ──
printStatus(ctx)
}
// scanDevices 扫描并更新设备列表
// 设备身份 = MACIP 变化视为同一设备(更新 IPIP 被复用则移除旧设备
func scanDevices(ctx *Context) {
scanner := discovery.NewScanner(
ctx.Config.Scan.CIDR,
ctx.Config.SSH.Port,
ctx.Config.Scan.Timeout,
ctx.Config.Scan.Concurrency,
)
hosts := scanner.Scan()
// 本轮扫描到的 IP → MAC 映射
ipToMAC := make(map[string]string, len(hosts))
for _, h := range hosts {
ipToMAC[h.IP.String()] = h.MAC
}
// 1. 处理 IP 被复用:旧设备的 IP 现在属于不同 MAC → 移除
for mac, dev := range ctx.Devices {
if newMAC, ok := ipToMAC[dev.IP]; ok && newMAC != mac {
delete(ctx.Devices, mac)
delete(ctx.Results, mac)
delete(ctx.Printed, mac)
fmt.Printf(" [-] IP 复用: %s 现属于 %s移除旧设备 %s\n", dev.IP, newMAC, mac)
}
}
// 2. 合并扫描结果
added, updated := 0, 0
for _, h := range hosts {
mac := h.MAC
ip := h.IP.String()
if mac == "" {
// 无法获取 MAC降级用 IP 做 key标记为临时
mac = "tmp:" + ip
}
if dev, ok := ctx.Devices[mac]; ok {
// 已存在:仅更新 IP设备可能换了地址
if dev.IP != ip {
dev.IP = ip
dev.Host = h
updated++
}
} else {
// 新设备
ctx.Devices[mac] = &Device{MAC: mac, IP: ip, Host: h}
added++
}
}
fmt.Printf(" [扫描] 设备 %d 台(新增 %dIP更新 %d\n", len(ctx.Devices), added, updated)
}
// testPending 对未测试或失败设备执行测试
func testPending(ctx *Context) {
for mac, dev := range ctx.Devices {
// 跳过: 已有结果且通过
if rpt, ok := ctx.Results[mac]; ok && rpt != nil && rpt.Failed == 0 && rpt.Errors == 0 {
continue
}
// 需要测试(结果为空 或 失败)
fmt.Printf(" [测试] %s (%s) 开始压测...\n", dev.IP, dev.MAC)
rpt := testDevice(ctx, dev.IP)
ctx.Results[mac] = rpt
// 保存测试报告
saveDeviceReport(dev, rpt, ctx.Config.Report.Path)
}
}
// testDevice 对单台设备执行 SSH 登录 + 压力测试
func testDevice(ctx *Context, ip string) *stress.Report {
cfg := ctx.Config
// SSH 连接
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 &stress.Report{
IP: ip,
StartTime: time.Now(),
Results: []stress.Result{{
Type: "ssh",
Status: "fail",
Error: err.Error(),
}},
Failed: 1,
}
}
defer conn.Close()
// 执行压力测试
runner := stress.NewRunner(
stress.Config{
Duration: cfg.Stress.Duration,
Threads: cfg.Stress.Threads,
DiskSizeMB: 1024,
TempLogInt: 10 * time.Second,
},
mapStressTypes(cfg.Stress.Types),
client,
)
return runner.Run(conn, ip)
}
// printStatus 打印状态变化(仅当首次或状态变化时)
func printStatus(ctx *Context) {
for mac, rpt := range ctx.Results {
dev := ctx.Devices[mac]
label := devLabel(dev)
status := statusOf(rpt)
// 上次状态为空 或 与当前不一致 → 打印并记录
if prev, ok := ctx.Printed[mac]; !ok || prev != status {
icon := map[string]string{
StatusPass: "✓",
StatusFail: "✗",
}[status]
fmt.Printf(" [状态] %s %s → %s\n", label, icon, status)
ctx.Printed[mac] = status
}
}
// 未测试设备(无结果): 首次打印一次 untested
for mac, dev := range ctx.Devices {
if _, hasResult := ctx.Results[mac]; !hasResult {
if _, printed := ctx.Printed[mac]; !printed {
fmt.Printf(" [状态] %s ○ → %s\n", devLabel(dev), StatusUntested)
ctx.Printed[mac] = StatusUntested
}
}
}
}
// devLabel 设备显示名: MAC (IP)
func devLabel(dev *Device) string {
if dev == nil {
return "?"
}
if strings.HasPrefix(dev.MAC, "tmp:") {
return dev.IP + " (无MAC)"
}
return fmt.Sprintf("%s (%s)", dev.MAC, dev.IP)
}
// mapStressTypes 逗号分隔字符串 → TestType 列表
func mapStressTypes(s string) []stress.TestType {
var types []stress.TestType
for _, t := range strings.Split(s, ",") {
t = strings.TrimSpace(t)
types = append(types, stress.TestType(t))
}
return types
}
// statusOf 从测试报告得出状态
func statusOf(rpt *stress.Report) string {
if rpt == nil {
return StatusUntested
}
if rpt.Failed > 0 || rpt.Errors > 0 {
return StatusFail
}
return StatusPass
}
// saveDeviceReport 保存单台设备的测试报告(文件名含 MAC 和 IP
func saveDeviceReport(dev *Device, rpt *stress.Report, dir string) {
if dir == "" {
dir = "reports"
}
ts := time.Now().Format("20060102-150405")
name := strings.ReplaceAll(dev.MAC, ":", "")
path := fmt.Sprintf("%s/report-%s-%s-%s.txt", strings.TrimRight(dir, "/"), name, dev.IP, ts)
r := report.NewBuilder(report.ConfigSummary{})
r.StartPhase("压力测试")
for _, res := range rpt.Results {
r.AddStressResult(dev.IP, report.StressResult{
Type: string(res.Type),
Status: res.Status,
Duration: res.Duration.String(),
Error: res.Error,
Output: res.Output,
})
}
r.EndPhase(rpt.Summary())
if err := r.Build().SaveFile(path, "text"); err != nil {
fmt.Printf(" [报告] %s 保存失败: %v\n", dev.IP, err)
} else {
fmt.Printf(" [报告] %s 已保存: %s\n", dev.IP, path)
}
}