From 906bb54aaa9b5f6732653378e4c090a8714c0a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=A8=8133321?= <33321@sangfor.com> Date: Wed, 12 Aug 2026 17:11:59 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=20auto-check=EF=BC=9A?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=95=B0=E6=8D=AE=E4=B8=8B=E6=B2=89=E5=90=84?= =?UTF-8?q?=E5=8C=85=EF=BC=8Cworkflow=20=E7=B2=BE=E7=AE=80=E4=B8=BA?= =?UTF-8?q?=E7=BA=AF=E8=B0=83=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - discovery: ping 探测存活 + ARP 解析 MAC,包级 Devices map,串行扫描 - stress: 脚本生成模式(BuildScript/ParseResults),ToolSet 工具检测,包级 Results map - report: 包级 Printed map,SaveAndPrintDeviceReport 保存+打印 - sshclient: 包级 RunCommand - workflow: 仅持有 config,Start() 固定循环,直接调用各包方法 - config: 移除 ScanConfig.Concurrency(串行扫描无需并发数) --- auto-check/auto-check.yaml | 1 - auto-check/pkg/config/config.go | 11 +- auto-check/pkg/discovery/arp.go | 77 +++++++ auto-check/pkg/discovery/discover.go | 23 ++ auto-check/pkg/discovery/discovery.go | 230 ------------------- auto-check/pkg/discovery/scanner.go | 106 +++++++++ auto-check/pkg/report/print.go | 1 - auto-check/pkg/sshclient/sshclient.go | 35 ++- auto-check/pkg/stress/metrics.go | 74 ++++++ auto-check/pkg/stress/runner.go | 312 ++++++++++++++------------ auto-check/pkg/stress/runners.go | 168 -------------- auto-check/pkg/stress/script.go | 156 +++++++++++++ auto-check/pkg/stress/tools.go | 287 ++++------------------- auto-check/pkg/stress/types.go | 96 +++----- auto-check/pkg/workflow/workflow.go | 2 +- 15 files changed, 708 insertions(+), 871 deletions(-) create mode 100644 auto-check/pkg/discovery/arp.go create mode 100644 auto-check/pkg/discovery/discover.go delete mode 100644 auto-check/pkg/discovery/discovery.go create mode 100644 auto-check/pkg/discovery/scanner.go create mode 100644 auto-check/pkg/stress/metrics.go delete mode 100644 auto-check/pkg/stress/runners.go create mode 100644 auto-check/pkg/stress/script.go diff --git a/auto-check/auto-check.yaml b/auto-check/auto-check.yaml index 8294f88..f188ce4 100644 --- a/auto-check/auto-check.yaml +++ b/auto-check/auto-check.yaml @@ -5,7 +5,6 @@ scan: cidr: "192.168.1.0/24" timeout: 3s - concurrency: 50 # --- SSH 认证 --- ssh: diff --git a/auto-check/pkg/config/config.go b/auto-check/pkg/config/config.go index 6f971ef..1cd9c47 100644 --- a/auto-check/pkg/config/config.go +++ b/auto-check/pkg/config/config.go @@ -27,9 +27,8 @@ type Config struct { // ScanConfig 网段扫描 type ScanConfig struct { - CIDR string `yaml:"cidr"` - Timeout time.Duration `yaml:"timeout"` - Concurrency int `yaml:"concurrency"` + CIDR string `yaml:"cidr"` + Timeout time.Duration `yaml:"timeout"` } // SSHConfig SSH 认证 @@ -59,7 +58,6 @@ type PrintConfig struct { Backend string `yaml:"backend"` // system / escpos / usb / file Device string `yaml:"device"` // 目标:打印机名 / host:port / 设备路径 / 文件路径 Width int `yaml:"width"` // 行宽,80mm=42 - Charset string `yaml:"charset"` // 字符集(默认 utf-8) } // WorkflowConfig 工作流 @@ -75,9 +73,8 @@ type WorkflowConfig struct { func Default() *Config { return &Config{ Scan: ScanConfig{ - CIDR: "192.168.1.0/24", - Timeout: 3 * time.Second, - Concurrency: 50, + CIDR: "192.168.1.0/24", + Timeout: 3 * time.Second, }, SSH: SSHConfig{ User: "root", diff --git a/auto-check/pkg/discovery/arp.go b/auto-check/pkg/discovery/arp.go new file mode 100644 index 0000000..75593d1 --- /dev/null +++ b/auto-check/pkg/discovery/arp.go @@ -0,0 +1,77 @@ +package discovery + +import ( + "os" + "os/exec" + "runtime" + "strings" +) + +// lookupMAC 从 ARP 表获取 IP 对应的 MAC 地址(跨平台) +// 返回空字符串表示无法获取 +func lookupMAC(ip string) string { + switch runtime.GOOS { + case "linux": + return macFromProcNetArp(ip) + case "windows": + return macFromWindowsArp(ip) + default: + return macFromUnixArp(ip) + } +} + +// macFromProcNetArp 解析 Linux /proc/net/arp +func macFromProcNetArp(ip string) string { + data, err := os.ReadFile("/proc/net/arp") + if err != nil { + return "" + } + for _, line := range strings.Split(string(data), "\n")[1:] { + fields := strings.Fields(line) + if len(fields) >= 4 && fields[0] == ip { + mac := fields[3] + if mac != "" && mac != "00:00:00:00:00:00" { + return mac + } + } + } + return "" +} + +// macFromWindowsArp 解析 Windows `arp -a` 输出 +func macFromWindowsArp(ip string) string { + out, err := exec.Command("arp", "-a").Output() + if err != nil { + return "" + } + for _, line := range strings.Split(string(out), "\n") { + fields := strings.Fields(line) + if len(fields) >= 2 && fields[0] == ip { + mac := strings.ReplaceAll(fields[1], "-", ":") + if mac != "" && mac != "00:00:00:00:00:00" { + return strings.ToLower(mac) + } + } + } + return "" +} + +// macFromUnixArp 解析 macOS/Linux `arp -n` 输出 +func macFromUnixArp(ip string) string { + out, err := exec.Command("arp", "-n", ip).Output() + if err != nil { + return "" + } + for _, line := range strings.Split(string(out), "\n") { + fields := strings.Fields(line) + for i, f := range fields { + if f == "("+ip+")" && i+2 < len(fields) { + mac := fields[i+2] + if mac != "" && mac != "ff:ff:ff:ff:ff:ff" { + return strings.ToLower(mac) + } + } + } + } + return "" +} diff --git a/auto-check/pkg/discovery/discover.go b/auto-check/pkg/discovery/discover.go new file mode 100644 index 0000000..021b805 --- /dev/null +++ b/auto-check/pkg/discovery/discover.go @@ -0,0 +1,23 @@ +package discovery + +import ( + "fmt" + + "auto-check/pkg/model" +) + +// Devices 设备表(IP → 设备),每轮 Discover() 后更新,包级公开 +var Devices = make(map[string]model.Device) + +// Discover 扫描并更新包级 Devices map(过滤无 MAC 设备,保留上次状态延续) +func (s *Scanner) Discover() { + scanned := s.Scan() + Devices = make(map[string]model.Device, len(scanned)) + for _, dev := range scanned { + if dev.MAC == "" { + fmt.Printf(" [!] %s 无 MAC(ARP 未解析),下轮重试\n", dev.IP) + continue + } + Devices[dev.IP] = dev + } +} diff --git a/auto-check/pkg/discovery/discovery.go b/auto-check/pkg/discovery/discovery.go deleted file mode 100644 index aa59577..0000000 --- a/auto-check/pkg/discovery/discovery.go +++ /dev/null @@ -1,230 +0,0 @@ -package discovery - -import ( - "fmt" - "net" - "os" - "os/exec" - "runtime" - "strings" - "sync" - "time" - - "auto-check/pkg/model" -) - -// Devices 设备表(IP → 设备),每轮 Discover() 后更新,包级公开 -var Devices = make(map[string]model.Device) - -// Scanner 网段扫描器 -type Scanner struct { - CIDR string - Timeout time.Duration - Concurrency int -} - -// NewScanner 创建扫描器 -func NewScanner(cidr string, timeout time.Duration, concurrency int) *Scanner { - return &Scanner{ - CIDR: cidr, - Timeout: timeout, - Concurrency: concurrency, - } -} - -// Scan 扫描网段,返回存活设备列表 -func (s *Scanner) Scan() []model.Device { - ips, ok := s.candidateIPs() - if !ok { - return nil - } - - fmt.Printf("[扫描] 网段 %s,共 %d 个IP,开始探测...\n", s.CIDR, len(ips)) - - results := s.probeAll(ips) - - fmt.Printf("[扫描] 完成,发现 %d 台存活设备\n", len(results)) - return results -} - -// candidateIPs 解析网段并生成候选 IP 列表(排除网络地址和广播地址) -// 网段解析失败时返回 false -func (s *Scanner) candidateIPs() ([]net.IP, bool) { - ip, ipnet, err := net.ParseCIDR(s.CIDR) - if err != nil { - fmt.Printf("[扫描] 网段解析失败: %v\n", err) - return nil, false - } - - var ips []net.IP - for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) { - dst := make(net.IP, len(ip)) - copy(dst, ip) - ips = append(ips, dst) - } - // 排除网络地址和广播地址 - if len(ips) > 2 { - ips = ips[1 : len(ips)-1] - } - return ips, true -} - -// probeAll 并发探测所有候选 IP,返回存活设备列表 -func (s *Scanner) probeAll(ips []net.IP) []model.Device { - var mu sync.Mutex - var wg sync.WaitGroup - var results []model.Device - - sem := make(chan struct{}, s.Concurrency) - for _, ip := range ips { - wg.Add(1) - sem <- struct{}{} - go func(target net.IP) { - defer wg.Done() - defer func() { <-sem }() - - dev, ok := s.probeDevice(target) - if !ok { - return - } - mu.Lock() - results = append(results, dev) - mu.Unlock() - desc := "无MAC" - if dev.MAC != "" { - desc = dev.MAC - } - fmt.Printf(" [+] %s 存活 (MAC: %s)\n", target, desc) - }(ip) - } - wg.Wait() - return results -} - -// probeDevice 探测单个 IP,存活则返回设备信息 -func (s *Scanner) probeDevice(ip net.IP) (model.Device, bool) { - if !s.probe(ip) { - return model.Device{}, false - } - // 获取 MAC 地址(ping 探测已触发 ARP 解析) - dev := model.Device{ - IP: ip.String(), - MAC: getMAC(ip.String()), - } - return dev, true -} - -// probe 用 ping 探测单个 IP 是否存活,返回 bool -func (s *Scanner) probe(ip net.IP) bool { - waitMs := s.Timeout.Milliseconds() - if waitMs < 1000 { - waitMs = 1000 // ping 等待至少 1 秒 - } - // 各平台 ping 参数不同:Linux -W 秒,macOS/Windows -W/-w 毫秒 - var args []string - switch runtime.GOOS { - case "windows": - args = []string{"-n", "1", "-w", fmt.Sprintf("%d", waitMs), ip.String()} - case "darwin": - args = []string{"-c", "1", "-W", fmt.Sprintf("%d", waitMs), ip.String()} - default: - args = []string{"-c", "1", "-W", fmt.Sprintf("%d", waitMs/1000), ip.String()} - } - return exec.Command("ping", args...).Run() == nil -} - -// getMAC 从 ARP 表获取 IP 对应的 MAC 地址(跨平台) -// 返回空字符串表示无法获取 -func getMAC(ip string) string { - switch runtime.GOOS { - case "linux": - return macFromProcNetArp(ip) - case "windows": - return macFromWindowsArp(ip) - case "darwin": - return macFromUnixArp(ip) - default: - return macFromUnixArp(ip) - } -} - -// macFromProcNetArp 解析 Linux /proc/net/arp -func macFromProcNetArp(ip string) string { - data, err := os.ReadFile("/proc/net/arp") - if err != nil { - return "" - } - for _, line := range strings.Split(string(data), "\n")[1:] { - fields := strings.Fields(line) - if len(fields) >= 4 && fields[0] == ip { - mac := fields[3] - if mac != "" && mac != "00:00:00:00:00:00" { - return mac - } - } - } - return "" -} - -// macFromWindowsArp 解析 Windows `arp -a` 输出 -func macFromWindowsArp(ip string) string { - out, err := exec.Command("arp", "-a").Output() - if err != nil { - return "" - } - for _, line := range strings.Split(string(out), "\n") { - fields := strings.Fields(line) - // Windows 格式: IP MAC Type - if len(fields) >= 2 && fields[0] == ip { - mac := strings.ReplaceAll(fields[1], "-", ":") - if mac != "" && mac != "00:00:00:00:00:00" { - return strings.ToLower(mac) - } - } - } - return "" -} - -// macFromUnixArp 解析 macOS/Linux `arp -n` 输出 -func macFromUnixArp(ip string) string { - out, err := exec.Command("arp", "-n", ip).Output() - if err != nil { - return "" - } - for _, line := range strings.Split(string(out), "\n") { - fields := strings.Fields(line) - // macOS 格式: ? (IP) at MAC on en0 ifscope [ethernet] - for i, f := range fields { - if f == "("+ip+")" && i+2 < len(fields) { - mac := fields[i+2] - if mac != "" && mac != "ff:ff:ff:ff:ff:ff" { - return strings.ToLower(mac) - } - } - } - } - return "" -} - -// Discover 扫描并更新包级 Devices map(过滤无 MAC 设备) -func (s *Scanner) Discover() { - devices := s.Scan() - Devices = make(map[string]model.Device, len(devices)) - for _, dev := range devices { - if dev.MAC == "" { - fmt.Printf(" [!] %s 无 MAC(ARP 未解析),下轮重试\n", dev.IP) - continue - } - Devices[dev.IP] = dev - } -} - -// inc IP 递增 -func inc(ip net.IP) { - for j := len(ip) - 1; j >= 0; j-- { - ip[j]++ - if ip[j] > 0 { - break - } - } -} diff --git a/auto-check/pkg/discovery/scanner.go b/auto-check/pkg/discovery/scanner.go new file mode 100644 index 0000000..2b548b0 --- /dev/null +++ b/auto-check/pkg/discovery/scanner.go @@ -0,0 +1,106 @@ +package discovery + +import ( + "fmt" + "net" + "os/exec" + "runtime" + "time" + + "auto-check/pkg/model" +) + +// Scanner 网段扫描器(串行,按顺序逐个探测) +type Scanner struct { + CIDR string + Timeout time.Duration +} + +// NewScanner 创建扫描器 +func NewScanner(cidr string, timeout time.Duration) *Scanner { + return &Scanner{ + CIDR: cidr, + Timeout: timeout, + } +} + +// Scan 扫描网段,返回存活设备列表(含 MAC 地址) +// 串行逐个探测,每个 IP 有超时保护 +func (s *Scanner) Scan() []model.Device { + ips, ok := s.candidateIPs() + if !ok { + return nil + } + fmt.Printf("[扫描] 网段 %s,共 %d 个IP,开始探测...\n", s.CIDR, len(ips)) + + var results []model.Device + for _, ip := range ips { + if dev, ok := s.probe(ip); ok { + results = append(results, dev) + } + } + + fmt.Printf("[扫描] 完成,发现 %d 台存活设备\n", len(results)) + return results +} + +// candidateIPs 解析网段并生成候选 IP 列表(排除网络地址和广播地址) +func (s *Scanner) candidateIPs() ([]net.IP, bool) { + ip, ipnet, err := net.ParseCIDR(s.CIDR) + if err != nil { + fmt.Printf("[扫描] 网段解析失败: %v\n", err) + return nil, false + } + var ips []net.IP + for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) { + dst := make(net.IP, len(ip)) + copy(dst, ip) + ips = append(ips, dst) + } + if len(ips) > 2 { + ips = ips[1 : len(ips)-1] + } + return ips, true +} + +// probe 探测单个 IP,存活则返回设备信息(MAC 通过 ARP 表查询) +func (s *Scanner) probe(ip net.IP) (model.Device, bool) { + if !s.isAlive(ip) { + return model.Device{}, false + } + mac := lookupMAC(ip.String()) + desc := "无MAC" + if mac != "" { + desc = mac + } + fmt.Printf(" [+] %s 存活 (MAC: %s)\n", ip, desc) + return model.Device{IP: ip.String(), MAC: mac}, true +} + +// isAlive 用 ping 探测单个 IP 是否存活(内置超时保护) +func (s *Scanner) isAlive(ip net.IP) bool { + waitMs := s.Timeout.Milliseconds() + if waitMs < 1000 { + waitMs = 1000 + } + var args []string + switch runtime.GOOS { + case "windows": + args = []string{"-n", "1", "-w", fmt.Sprintf("%d", waitMs), ip.String()} + case "darwin": + args = []string{"-c", "1", "-W", fmt.Sprintf("%d", waitMs), ip.String()} + default: + args = []string{"-c", "1", "-W", fmt.Sprintf("%d", waitMs/1000), ip.String()} + } + return exec.Command("ping", args...).Run() == nil +} + +// inc IP 递增 +func inc(ip net.IP) { + for j := len(ip) - 1; j >= 0; j-- { + ip[j]++ + if ip[j] > 0 { + break + } + } +} diff --git a/auto-check/pkg/report/print.go b/auto-check/pkg/report/print.go index 28b8009..2ee9350 100644 --- a/auto-check/pkg/report/print.go +++ b/auto-check/pkg/report/print.go @@ -37,7 +37,6 @@ func DefaultPrintConfig() config.PrintConfig { return config.PrintConfig{ Backend: "system", Width: 42, - Charset: "utf-8", } } diff --git a/auto-check/pkg/sshclient/sshclient.go b/auto-check/pkg/sshclient/sshclient.go index e0b6f1c..c12573e 100644 --- a/auto-check/pkg/sshclient/sshclient.go +++ b/auto-check/pkg/sshclient/sshclient.go @@ -2,14 +2,15 @@ package sshclient import ( "fmt" + "net" "os" "time" "golang.org/x/crypto/ssh" ) -// Client SSH 客户端封装 -type Client struct { +// Config SSH 连接配置(不含连接状态) +type Config struct { User string Password string KeyFile string @@ -17,9 +18,9 @@ type Client struct { Timeout time.Duration } -// NewClient 创建 SSH 客户端 -func NewClient(user, password, keyFile string, port int, timeout time.Duration) *Client { - return &Client{ +// NewConfig 创建配置 +func NewConfig(user, password, keyFile string, port int, timeout time.Duration) *Config { + return &Config{ User: user, Password: password, KeyFile: keyFile, @@ -28,23 +29,23 @@ func NewClient(user, password, keyFile string, port int, timeout time.Duration) } } -// Connect 尝试 SSH 连接,返回 SSH client -func (c *Client) Connect(ip string) (*ssh.Client, error) { - config, err := c.buildConfig() +// Connect 建立 SSH 连接,由调用方负责 Close +func Connect(ip string, cfg *Config) (*ssh.Client, error) { + sshCfg, err := cfg.buildSSHConfig() if err != nil { - return nil, fmt.Errorf("SSH 配置构建失败: %w", err) + return nil, err } - addr := fmt.Sprintf("%s:%d", ip, c.Port) - client, err := ssh.Dial("tcp", addr, config) + addr := net.JoinHostPort(ip, fmt.Sprintf("%d", cfg.Port)) + client, err := ssh.Dial("tcp", addr, sshCfg) if err != nil { return nil, fmt.Errorf("SSH 连接 %s 失败: %w", addr, err) } return client, nil } -// RunCommand 在远程主机执行命令,返回输出 -func (c *Client) RunCommand(client *ssh.Client, command string) (string, error) { +// RunCommand 在已有 SSH 连接上执行命令,返回输出(实现 stress.Executor 接口) +func RunCommand(client *ssh.Client, command string) (string, error) { session, err := client.NewSession() if err != nil { return "", fmt.Errorf("创建会话失败: %w", err) @@ -53,21 +54,19 @@ func (c *Client) RunCommand(client *ssh.Client, command string) (string, error) out, err := session.CombinedOutput(command) if err != nil { - return string(out), fmt.Errorf("命令执行失败: %w, 输出: %s", err, string(out)) + return string(out), fmt.Errorf("命令执行失败: %w", err) } return string(out), nil } -// buildConfig 构建 SSH 认证配置 -func (c *Client) buildConfig() (*ssh.ClientConfig, error) { +// buildSSHConfig 构建认证配置 +func (c *Config) buildSSHConfig() (*ssh.ClientConfig, error) { var authMethods []ssh.AuthMethod - // 密码认证 if c.Password != "" { authMethods = append(authMethods, ssh.Password(c.Password)) } - // 密钥认证 if c.KeyFile != "" { key, err := os.ReadFile(c.KeyFile) if err != nil { diff --git a/auto-check/pkg/stress/metrics.go b/auto-check/pkg/stress/metrics.go new file mode 100644 index 0000000..e62cfa4 --- /dev/null +++ b/auto-check/pkg/stress/metrics.go @@ -0,0 +1,74 @@ +package stress + +import ( + "time" +) + +// ============================ +// 测试指标定义 +// ============================ + +// Metric 测试指标(定义测什么、用什么工具、怎么判结果) +type Metric struct { + Name string // 指标名称(cpu/memory/disk/memnative/full) + Tool string // 依赖的工具(stress-ng/stressapptest) + Enabled bool // 是否启用 + IsMonitor bool // 是否为监控指标(temp/dmesg,自动附加) +} + +// BuildMetrics 根据配置和可用工具,生成要执行的指标列表 +func BuildMetrics(types []TestType, tools ToolSet) []Metric { + var metrics []Metric + + for _, t := range types { + m := Metric{Name: string(t), Enabled: true} + switch t { + case TestCPU: + m.Tool = "stress-ng" + case TestMemory: + m.Tool = "stress-ng" + case TestDiskIO: + m.Tool = "stress-ng" + case TestMemNative: + m.Tool = "stressapptest" + case TestFull: + m.Tool = "stress-ng" + default: + m.Enabled = false + } + if m.Tool != "" && !tools.Has(m.Tool) { + m.Enabled = false // 工具不可用,禁用 + } + metrics = append(metrics, m) + } + + // 自动附加监控指标 + metrics = append(metrics, Metric{Name: "temp", Tool: "lm-sensors", Enabled: tools.Has("lm-sensors"), IsMonitor: true}) + metrics = append(metrics, Metric{Name: "dmesg", Tool: "", Enabled: true, IsMonitor: true}) + + return metrics +} + +// ============================ +// 压力测试配置 +// ============================ + +// Config 压力测试配置 +type Config struct { + Duration time.Duration + Threads int + MemSizeMB int // 0=自动(可用60%) + DiskSizeMB int // 0=默认1024 + DiskDir string // 空=自动临时目录 + TempLogInt time.Duration // 0=10s +} + +// DefaultConfig 默认配置 +func DefaultConfig() Config { + return Config{ + Duration: 30 * time.Second, + Threads: 4, + DiskSizeMB: 1024, + TempLogInt: 10 * time.Second, + } +} diff --git a/auto-check/pkg/stress/runner.go b/auto-check/pkg/stress/runner.go index e58017b..10ef756 100644 --- a/auto-check/pkg/stress/runner.go +++ b/auto-check/pkg/stress/runner.go @@ -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 -} diff --git a/auto-check/pkg/stress/runners.go b/auto-check/pkg/stress/runners.go deleted file mode 100644 index fd7248e..0000000 --- a/auto-check/pkg/stress/runners.go +++ /dev/null @@ -1,168 +0,0 @@ -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} -} diff --git a/auto-check/pkg/stress/script.go b/auto-check/pkg/stress/script.go new file mode 100644 index 0000000..2829dae --- /dev/null +++ b/auto-check/pkg/stress/script.go @@ -0,0 +1,156 @@ +package stress + +import ( + "fmt" + "strings" +) + +// ============================ +// 脚本生成(根据指标生成远程执行的 shell 脚本) +// ============================ + +// BuildScript 根据指标列表和配置生成 self-contained shell 脚本 +// 脚本输出结构化文本,便于 ParseResults 解析 +func BuildScript(metrics []Metric, cfg Config) string { + var sb strings.Builder + + // 脚本头部 + sb.WriteString("#!/bin/bash\n") + sb.WriteString("set -e\n\n") + + // 系统信息探测 + sb.WriteString("echo '===SYSTEM_INFO==='\n") + sb.WriteString("echo hostname:$(hostname)\n") + sb.WriteString("echo cpu:$(lscpu 2>/dev/null | grep 'Model name' | sed 's/Model name:\\s*//' || echo unknown)\n") + sb.WriteString("echo cores:$(nproc 2>/dev/null || echo 0)\n") + sb.WriteString("echo memory:$(free -h 2>/dev/null | awk '/Mem:/{print $2}' || echo unknown)\n") + sb.WriteString("echo kernel:$(uname -r 2>/dev/null || echo unknown)\n") + sb.WriteString("echo disk:$(lsblk -d -o NAME,SIZE 2>/dev/null | head -3 | tr '\\n' ' ')\n") + sb.WriteString("echo ''\n\n") + + // 启动温度监控(后台) + hasTemp := false + for _, m := range metrics { + if m.Name == "temp" && m.Enabled { + hasTemp = true + break + } + } + if hasTemp { + tempInterval := int(cfg.TempLogInt.Seconds()) + if tempInterval <= 0 { + tempInterval = 10 + } + sb.WriteString(fmt.Sprintf("# 温度监控(后台)\n")) + sb.WriteString("TEMP_LOG=/tmp/auto-check-temp.log\n") + sb.WriteString("> $TEMP_LOG\n") + sb.WriteString(fmt.Sprintf("(while true; do sensors 2>/dev/null | grep -i 'temp\\|core\\|cpu' | head -5 >> $TEMP_LOG; sleep %d; done) &\n", tempInterval)) + sb.WriteString("TEMP_PID=$!\n\n") + } + + // 逐项执行测试 + for _, m := range metrics { + if m.IsMonitor || !m.Enabled { + continue + } + sb.WriteString(fmt.Sprintf("echo '===TEST:%s==='\n", m.Name)) + sb.WriteString("START_TIME=$(date +%%s%%N)\n") + sb.WriteString("set +e\n") + + cmd := buildTestCommand(m, cfg) + sb.WriteString(fmt.Sprintf("OUTPUT=$(%s 2>&1)\n", cmd)) + sb.WriteString("EXIT_CODE=$?\n") + sb.WriteString("set -e\n") + sb.WriteString("END_TIME=$(date +%%s%%N)\n") + sb.WriteString("DURATION=$(( (END_TIME - START_TIME) / 1000000 ))\n") + + // 判定结果 + sb.WriteString("if [ $EXIT_CODE -eq 0 ]; then\n") + sb.WriteString(" echo 'status:pass'\n") + sb.WriteString("else\n") + sb.WriteString(" echo 'status:fail'\n") + sb.WriteString("fi\n") + sb.WriteString("echo \"duration:${DURATION}ms\"\n") + sb.WriteString("echo \"output:${OUTPUT}\"\n") + sb.WriteString("echo ''\n\n") + } + + // 收集温度监控结果 + if hasTemp { + sb.WriteString("echo '===MONITOR:temp==='\n") + sb.WriteString("if [ -f $TEMP_LOG ] && [ -s $TEMP_LOG ]; then\n") + sb.WriteString(" echo 'status:pass'\n") + sb.WriteString(" TEMP_MAX=$(grep -oP '\\d+\\.?\\d*°C' $TEMP_LOG | sort -t. -k1 -n | tail -1 || echo 'unknown')\n") + sb.WriteString(" echo \"samples:$(wc -l < $TEMP_LOG)\"\n") + sb.WriteString(" echo \"max:$TEMP_MAX\"\n") + sb.WriteString(" echo \"output:$(tail -10 $TEMP_LOG)\"\n") + sb.WriteString("else\n") + sb.WriteString(" echo 'status:skip'\n") + sb.WriteString("fi\n") + sb.WriteString("echo ''\n") + sb.WriteString("kill $TEMP_PID 2>/dev/null || true\n\n") + } + + // 收集 dmesg 结果 + sb.WriteString("echo '===MONITOR:dmesg==='\n") + sb.WriteString("DMESG_ERR=$(dmesg --level=err,crit,alert,emerg 2>/dev/null | tail -30 || true)\n") + sb.WriteString("if [ -n \"$DMESG_ERR\" ]; then\n") + sb.WriteString(" echo 'status:fail'\n") + sb.WriteString(" echo \"output:$DMESG_ERR\"\n") + sb.WriteString("else\n") + sb.WriteString(" echo 'status:pass'\n") + sb.WriteString(" echo 'output:无硬件相关内核报错'\n") + sb.WriteString("fi\n") + sb.WriteString("echo ''\n") + sb.WriteString("echo '===END==='\n") + + return sb.String() +} + +// buildTestCommand 根据指标生成测试命令 +func buildTestCommand(m Metric, cfg Config) string { + sec := int(cfg.Duration.Seconds()) + threads := cfg.Threads + + switch m.Name { + case "cpu": + return fmt.Sprintf("stress-ng --cpu %d --cpu-method all --timeout %ds --metrics-brief --temp-path /tmp", threads, sec) + + case "memory": + workers := threads + if workers > 4 { + workers = 4 + } + return fmt.Sprintf("stress-ng --vm %d --vm-bytes 256M --vm-method all --timeout %ds --metrics-brief", workers, sec) + + case "disk": + testDir := cfg.DiskDir + if testDir == "" { + testDir = "/tmp/stress-disk-test" + } + diskMB := cfg.DiskSizeMB + if diskMB <= 0 { + diskMB = 1024 + } + return fmt.Sprintf("mkdir -p %s && stress-ng --iomix 2 --iomix-bytes %dM --timeout %ds --metrics-brief && rm -rf %s", + testDir, diskMB, sec, testDir) + + case "memnative": + memMB := cfg.MemSizeMB + if memMB <= 0 { + memMB = 0 // 脚本内自动计算 + } + if memMB > 0 { + return fmt.Sprintf("stressapptest -s %d -M %d -f 0 -v", sec, memMB) + } + // 自动计算可用内存的 60% + return fmt.Sprintf("M=$(free -m 2>/dev/null | awk '/Mem:/{print int($7*0.6)}' || echo 512) && stressapptest -s %d -M $M -f 0 -v", sec) + + case "full": + return fmt.Sprintf("stress-ng --cpu %d --vm 2 --vm-bytes 128M --iomix 1 --iomix-bytes 256M --timeout %ds --metrics-brief", + threads, sec) + + default: + return "echo '未知测试类型'" + } +} diff --git a/auto-check/pkg/stress/tools.go b/auto-check/pkg/stress/tools.go index 096eecd..39722d9 100644 --- a/auto-check/pkg/stress/tools.go +++ b/auto-check/pkg/stress/tools.go @@ -1,269 +1,78 @@ package stress import ( - "fmt" "strings" - "time" + + "auto-check/pkg/sshclient" "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 + Name string + 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) +// ToolSet 远程可用工具集合 +type ToolSet struct { + Tools map[string]ToolInfo // name → info +} + +// Has 工具是否可用 +func (ts ToolSet) Has(name string) bool { + _, ok := ts.Tools[name] + return ok +} + +// Get 获取工具信息 +func (ts ToolSet) Get(name string) (ToolInfo, bool) { + t, ok := ts.Tools[name] + return t, ok +} + +// DetectTools 检测远程所有相关工具是否可用 +func DetectTools(client *ssh.Client) ToolSet { + ts := ToolSet{Tools: make(map[string]ToolInfo)} + for _, name := range []string{"stress-ng", "stressapptest", "iperf3", "fio", "lm-sensors"} { + if info, ok := detectOne(client, name); ok { + ts.Tools[name] = info + } + } + return ts +} + +// detectOne 检测单个工具 +func detectOne(client *ssh.Client, name string) (ToolInfo, bool) { + out, err := sshclient.RunCommand(client, "which "+name+" 2>/dev/null") path := strings.TrimSpace(out) if err != nil || path == "" { - return ToolInfo{Available: false} + return ToolInfo{}, false } - // 获取版本 var version string switch name { case "stress-ng": - out, _ = execf(client, executor, "%s --version 2>&1 | head -1", path) + out, _ = sshclient.RunCommand(client, path+" --version 2>&1 | head -1") version = strings.TrimSpace(out) case "stressapptest": - out, _ = execf(client, executor, "%s --help 2>&1 | head -1", path) + out, _ = sshclient.RunCommand(client, path+" --help 2>&1 | head -1") + version = strings.TrimSpace(out) + case "iperf3": + out, _ = sshclient.RunCommand(client, path+" --version 2>&1 | head -1") + version = strings.TrimSpace(out) + case "fio": + out, _ = sshclient.RunCommand(client, path+" --version 2>&1 | head -1") version = strings.TrimSpace(out) case "lm-sensors": - out, _ = execf(client, executor, "%s -v 2>&1 | head -1", path) + out, _ = sshclient.RunCommand(client, path+" -v 2>&1 | head -1") 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)) - } - } - } - } - } + return ToolInfo{Name: name, Path: path, Version: version}, true } diff --git a/auto-check/pkg/stress/types.go b/auto-check/pkg/stress/types.go index 7aed7e7..1ca82d6 100644 --- a/auto-check/pkg/stress/types.go +++ b/auto-check/pkg/stress/types.go @@ -6,72 +6,38 @@ import ( "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 内核报错监控 + TestCPU TestType = "cpu" + TestMemory TestType = "memory" + TestDiskIO TestType = "disk" + TestMemNative TestType = "memnative" + TestFull TestType = "full" + MonitorTemp TestType = "temp" + MonitorDmesg TestType = "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, - } -} - -// ============================ -// 单项测试结果 -// ============================ +// 结果状态常量 +const ( + StatusPass = "pass" + StatusFail = "fail" + StatusSkip = "skip" + StatusError = "error" + StatusUnknown = "untested" +) // Result 单项测试结果 type Result struct { Type TestType - Status string // "pass" / "fail" / "skip" / "error" - Output string // 工具输出摘要 - Error string // 错误信息 + Status string + Output string + Error string Duration time.Duration } -// ============================ -// 完整测试报告 -// ============================ - -// Report 一次完整压测的报告 +// Report 完整压测报告 type Report struct { IP string StartTime time.Time @@ -84,17 +50,17 @@ type Report struct { Errors int } -// AddResult 添加测试结果 +// AddResult 添加测试结果并更新统计 func (r *Report) AddResult(res Result) { r.Results = append(r.Results, res) switch res.Status { - case "pass": + case StatusPass: r.Passed++ - case "fail": + case StatusFail: r.Failed++ - case "skip": + case StatusSkip: r.Skipped++ - case "error": + case StatusError: r.Errors++ } } @@ -114,19 +80,17 @@ func (r *Report) ToText() string { for _, res := range r.Results { icon := "✓" switch res.Status { - case "fail": + case StatusFail: icon = "✗" - case "skip": + case StatusSkip: icon = "⊘" - case "error": + case StatusError: 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)) - } + for _, line := range strings.Split(res.Output, "\n") { + if strings.TrimSpace(line) != "" { + sb.WriteString(fmt.Sprintf(" %s\n", line)) } } if res.Error != "" { diff --git a/auto-check/pkg/workflow/workflow.go b/auto-check/pkg/workflow/workflow.go index af63604..d56f811 100644 --- a/auto-check/pkg/workflow/workflow.go +++ b/auto-check/pkg/workflow/workflow.go @@ -60,7 +60,7 @@ func (w *Workflow) round() { // 1. 扫描(discovery 包),更新设备表 cfg := w.cfg - discovery.NewScanner(cfg.Scan.CIDR, cfg.Scan.Timeout, cfg.Scan.Concurrency).Discover() + discovery.NewScanner(cfg.Scan.CIDR, cfg.Scan.Timeout).Discover() fmt.Printf(" [扫描] 设备 %d 台\n", len(discovery.Devices)) // 2. 压测未通过设备(stress.TestDevice 执行,report 保存+打印)