Files
membank/auto-check/pkg/report/report.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

263 lines
7.0 KiB
Go

package report
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
// PhaseResult 单阶段结果
type PhaseResult struct {
Phase string `json:"phase" yaml:"phase"`
Status string `json:"status" yaml:"status"` // success / partial / failed
Hosts []HostResult `json:"hosts" yaml:"hosts"`
Summary string `json:"summary" yaml:"summary"`
}
// HostResult 单台主机结果
type HostResult struct {
IP string `json:"ip" yaml:"ip"`
Alive bool `json:"alive" yaml:"alive"`
OpenPort int `json:"open_port,omitempty" yaml:"open_port,omitempty"`
SSHLogin bool `json:"ssh_login" yaml:"ssh_login"`
SSHErr string `json:"ssh_error,omitempty" yaml:"ssh_error,omitempty"`
Stress []StressResult `json:"stress,omitempty" yaml:"stress,omitempty"`
}
// StressResult 单项压力测试结果
type StressResult struct {
Type string `json:"type" yaml:"type"`
Status string `json:"status" yaml:"status"` // pass / fail / skip
Duration string `json:"duration" yaml:"duration"`
Output string `json:"output,omitempty" yaml:"output,omitempty"`
Error string `json:"error,omitempty" yaml:"error,omitempty"`
}
// Report 检测报告
type Report struct {
StartTime time.Time `json:"start_time" yaml:"start_time"`
EndTime time.Time `json:"end_time" yaml:"end_time"`
Duration string `json:"duration" yaml:"duration"`
Config ConfigSummary `json:"config" yaml:"config"`
Phases []PhaseResult `json:"phases" yaml:"phases"`
}
// ConfigSummary 配置摘要(脱敏)
type ConfigSummary struct {
CIDR string `json:"cidr" yaml:"cidr"`
Port int `json:"port" yaml:"port"`
User string `json:"user" yaml:"user"`
Concurrency int `json:"concurrency" yaml:"concurrency"`
StressTypes string `json:"stress_types,omitempty" yaml:"stress_types,omitempty"`
}
// Builder 报告构建器
type Builder struct {
report *Report
phase *PhaseResult
}
// NewBuilder 创建报告构建器
func NewBuilder(cfg ConfigSummary) *Builder {
return &Builder{
report: &Report{
StartTime: time.Now(),
Config: cfg,
},
}
}
// StartPhase 开始新阶段
func (b *Builder) StartPhase(name string) {
b.phase = &PhaseResult{
Phase: name,
}
}
// AddHost 添加主机结果到当前阶段
func (b *Builder) AddHost(h HostResult) {
if b.phase != nil {
b.phase.Hosts = append(b.phase.Hosts, h)
}
}
// AddStressResult 为指定 IP 添加压力测试结果
func (b *Builder) AddStressResult(ip string, sr StressResult) {
if b.phase == nil {
return
}
for i := range b.phase.Hosts {
if b.phase.Hosts[i].IP == ip {
b.phase.Hosts[i].Stress = append(b.phase.Hosts[i].Stress, sr)
return
}
}
}
// EndPhase 结束当前阶段,设置状态和摘要
func (b *Builder) EndPhase(summary string) {
if b.phase == nil {
return
}
b.phase.Summary = summary
total := len(b.phase.Hosts)
success := 0
for _, h := range b.phase.Hosts {
switch b.phase.Phase {
case "设备发现":
if h.Alive {
success++
}
case "SSH 登录":
if h.SSHLogin {
success++
}
case "压力测试":
if len(h.Stress) > 0 {
success++
}
}
}
switch {
case success == total:
b.phase.Status = "success"
case success > 0:
b.phase.Status = "partial"
default:
b.phase.Status = "failed"
}
b.report.Phases = append(b.report.Phases, *b.phase)
b.phase = nil
}
// Build 生成最终报告
func (b *Builder) Build() *Report {
b.report.EndTime = time.Now()
b.report.Duration = b.report.EndTime.Sub(b.report.StartTime).Round(time.Millisecond).String()
return b.report
}
// ============================
// 输出格式化
// ============================
// ToText 输出纯文本报告
func (r *Report) ToText() string {
var sb strings.Builder
sb.WriteString("╔══════════════════════════════════════════════╗\n")
sb.WriteString("║ auto-check 检测报告 ║\n")
sb.WriteString("╚══════════════════════════════════════════════╝\n\n")
sb.WriteString(fmt.Sprintf(" 生成时间: %s\n", r.StartTime.Format("2006-01-02 15:04:05")))
sb.WriteString(fmt.Sprintf(" 总耗时: %s\n\n", r.Duration))
// 配置摘要
sb.WriteString("── 配置摘要 ──\n")
sb.WriteString(fmt.Sprintf(" 网段: %s\n", r.Config.CIDR))
sb.WriteString(fmt.Sprintf(" SSH端口: %d\n", r.Config.Port))
sb.WriteString(fmt.Sprintf(" SSH用户: %s\n", r.Config.User))
sb.WriteString(fmt.Sprintf(" 并发数: %d\n", r.Config.Concurrency))
if r.Config.StressTypes != "" {
sb.WriteString(fmt.Sprintf(" 压测类型: %s\n", r.Config.StressTypes))
}
sb.WriteString("\n")
// 各阶段结果
for _, phase := range r.Phases {
statusIcon := "✅"
switch phase.Status {
case "partial":
statusIcon = "⚠️"
case "failed":
statusIcon = "❌"
}
sb.WriteString(fmt.Sprintf("══ %s %s ══\n", phase.Phase, statusIcon))
sb.WriteString(fmt.Sprintf(" %s\n\n", phase.Summary))
if phase.Phase == "设备发现" || phase.Phase == "SSH 登录" {
for _, h := range phase.Hosts {
if phase.Phase == "设备发现" && !h.Alive {
continue
}
if phase.Phase == "SSH 登录" && !h.SSHLogin {
sb.WriteString(fmt.Sprintf(" ✗ %-15s %s\n", h.IP, h.SSHErr))
continue
}
icon := "✓"
if phase.Phase == "SSH 登录" {
icon = "✓"
}
sb.WriteString(fmt.Sprintf(" %s %-15s\n", icon, h.IP))
}
}
if phase.Phase == "压力测试" {
for _, h := range phase.Hosts {
if len(h.Stress) == 0 {
continue
}
sb.WriteString(fmt.Sprintf(" ┌─ %s\n", h.IP))
for _, s := range h.Stress {
icon := "✓"
if s.Status != "pass" {
icon = "✗"
}
sb.WriteString(fmt.Sprintf(" │ %s %-10s 耗时: %s\n", icon, s.Type, s.Duration))
if s.Error != "" {
sb.WriteString(fmt.Sprintf(" │ 错误: %s\n", s.Error))
}
}
sb.WriteString(" └──────────────\n")
}
}
sb.WriteString("\n")
}
return sb.String()
}
// ToJSON 输出 JSON 报告
func (r *Report) ToJSON() (string, error) {
data, err := json.MarshalIndent(r, "", " ")
if err != nil {
return "", err
}
return string(data), nil
}
// SaveFile 保存报告到文件
func (r *Report) SaveFile(path string, format string) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建目录失败: %w", err)
}
var content string
switch format {
case "json":
var err error
content, err = r.ToJSON()
if err != nil {
return err
}
default:
content = r.ToText()
}
return os.WriteFile(path, []byte(content), 0644)
}
// DefaultReportPath 默认报告路径
func DefaultReportPath() string {
timestamp := time.Now().Format("20060102-150405")
return filepath.Join(".", "reports", fmt.Sprintf("report-%s.txt", timestamp))
}