- 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 指定配置文件
263 lines
7.0 KiB
Go
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))
|
|
}
|