新增 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:
279
auto-check/pkg/workflow/loop.go
Normal file
279
auto-check/pkg/workflow/loop.go
Normal file
@@ -0,0 +1,279 @@
|
||||
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. 扫描设备,更新设备列表 map(MAC 为 key)──
|
||||
scanDevices(ctx)
|
||||
|
||||
// ── 2. 遍历设备: 结果为空或失败 → 开始测试,保存报告和结果 ──
|
||||
testPending(ctx)
|
||||
|
||||
// ── 3. 遍历打印: 状态变化才重新打印 ──
|
||||
printStatus(ctx)
|
||||
}
|
||||
|
||||
// scanDevices 扫描并更新设备列表
|
||||
// 设备身份 = MAC;IP 变化视为同一设备(更新 IP),IP 被复用则移除旧设备
|
||||
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 台(新增 %d,IP更新 %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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user