package syscheck import ( "context" "fmt" "strconv" "strings" ) // CheckResult represents the result of a system check type CheckResult struct { Name string Passed bool Actual string Expected string Message string } // DiskInfo represents disk information type DiskInfo struct { AvailableGB float64 WriteSpeed float64 // MB/s ReadSpeed float64 // MB/s } // MemInfo represents memory information type MemInfo struct { TotalGB float64 } // CPUInfo represents CPU information type CPUInfo struct { Cores int FrequencyMHz float64 } // CommandExecutor defines interface for executing commands type CommandExecutor interface { ExecuteCommand(ctx context.Context, cmds ...string) (string, error) } // CheckDiskSpace checks if disk space meets minimum requirements func CheckDiskSpace(ctx context.Context, executor CommandExecutor, minGB float64) (*CheckResult, error) { // Use df to check available disk space on root partition output, err := executor.ExecuteCommand(ctx, "df", "-BG", "/") if err != nil { return nil, fmt.Errorf("failed to check disk space: %w", err) } lines := strings.Split(strings.TrimSpace(output), "\n") if len(lines) < 2 { return nil, fmt.Errorf("unexpected df output format") } fields := strings.Fields(lines[1]) if len(fields) < 4 { return nil, fmt.Errorf("unexpected df fields count") } // Parse available space (4th field, format: "500G") availableStr := strings.TrimSuffix(fields[3], "G") available, err := strconv.ParseFloat(availableStr, 64) if err != nil { return nil, fmt.Errorf("failed to parse available disk space: %w", err) } result := &CheckResult{ Name: "Disk Space", Passed: available >= minGB, Actual: fmt.Sprintf("%.2f GB", available), Expected: fmt.Sprintf(">= %.2f GB", minGB), } if !result.Passed { result.Message = fmt.Sprintf("Insufficient disk space: %.2f GB available, %.2f GB required", available, minGB) } return result, nil } // CheckDiskPerformance checks disk read/write performance func CheckDiskPerformance(ctx context.Context, executor CommandExecutor, minWriteMBps, minReadMBps float64) (*CheckResult, error) { // Use dd to test write performance writeCmd := "dd if=/dev/zero of=/tmp/test_write bs=1M count=1024 oflag=direct 2>&1 | tail -1" writeOutput, err := executor.ExecuteCommand(ctx, "bash", "-c", writeCmd) if err != nil { return nil, fmt.Errorf("failed to check disk write performance: %w", err) } // Parse write speed from dd output (format: "... copied, X.XX s, XXX MB/s") writeSpeed, err := parseDDSpeed(writeOutput) if err != nil { return nil, fmt.Errorf("failed to parse write speed: %w, output: %s", err, writeOutput) } // Test read performance and clean up test file readCmd := "dd if=/tmp/test_write of=/dev/null bs=1M count=1024 iflag=direct 2>&1 | tail -1; rm -f /tmp/test_write" readOutput, err := executor.ExecuteCommand(ctx, "bash", "-c", readCmd) if err != nil { return nil, fmt.Errorf("failed to check disk read performance: %w", err) } // Parse read speed from dd output readSpeed, err := parseDDSpeed(readOutput) if err != nil { return nil, fmt.Errorf("failed to parse read speed: %w, output: %s", err, readOutput) } passed := writeSpeed >= minWriteMBps && readSpeed >= minReadMBps result := &CheckResult{ Name: "Disk Performance", Passed: passed, Actual: fmt.Sprintf("Write: %.2f MB/s, Read: %.2f MB/s", writeSpeed, readSpeed), Expected: fmt.Sprintf("Write: >= %.2f MB/s, Read: >= %.2f MB/s", minWriteMBps, minReadMBps), } if !passed { result.Message = fmt.Sprintf("Insufficient disk performance") } return result, nil } // parseDDSpeed parses the speed from dd command output // Expected format: "104857600 bytes (105 MB, 100 MiB) copied, 0.125749 s, 834 MB/s" func parseDDSpeed(output string) (float64, error) { output = strings.TrimSpace(output) if output == "" { return 0, fmt.Errorf("empty output") } // Find the last occurrence of "MB/s" or "GB/s" var speed float64 var unit string // Try to match "XXX MB/s" or "XXX GB/s" pattern if idx := strings.LastIndex(output, " MB/s"); idx != -1 { // Extract the number before " MB/s" fields := strings.Fields(output[:idx]) if len(fields) == 0 { return 0, fmt.Errorf("no speed value found") } speedStr := fields[len(fields)-1] var err error speed, err = strconv.ParseFloat(speedStr, 64) if err != nil { return 0, fmt.Errorf("failed to parse speed value '%s': %w", speedStr, err) } unit = "MB/s" } else if idx := strings.LastIndex(output, " GB/s"); idx != -1 { // Extract the number before " GB/s" fields := strings.Fields(output[:idx]) if len(fields) == 0 { return 0, fmt.Errorf("no speed value found") } speedStr := fields[len(fields)-1] var err error speed, err = strconv.ParseFloat(speedStr, 64) if err != nil { return 0, fmt.Errorf("failed to parse speed value '%s': %w", speedStr, err) } unit = "GB/s" speed *= 1024 // Convert GB/s to MB/s } else { return 0, fmt.Errorf("no MB/s or GB/s found in output") } if unit == "MB/s" || unit == "GB/s" { return speed, nil } return 0, fmt.Errorf("unexpected unit: %s", unit) } // CheckMemory checks if system memory meets minimum requirements func CheckMemory(ctx context.Context, executor CommandExecutor, minGB float64) (*CheckResult, error) { // Use free -m to check memory in MB for better precision output, err := executor.ExecuteCommand(ctx, "free", "-m") if err != nil { return nil, fmt.Errorf("failed to check memory: %w", err) } lines := strings.Split(strings.TrimSpace(output), "\n") if len(lines) < 2 { return nil, fmt.Errorf("unexpected free output format") } fields := strings.Fields(lines[1]) if len(fields) < 2 { return nil, fmt.Errorf("unexpected free fields count") } // Parse total memory in MB totalMB, err := strconv.ParseFloat(fields[1], 64) if err != nil { return nil, fmt.Errorf("failed to parse memory size: %w", err) } // Convert MB to GB (1 GB = 1024 MB) totalGB := totalMB / 1024.0 result := &CheckResult{ Name: "Memory Size", Passed: totalGB >= minGB, Actual: fmt.Sprintf("%.2f GB", totalGB), Expected: fmt.Sprintf(">= %.2f GB", minGB), } if !result.Passed { result.Message = fmt.Sprintf("Insufficient memory: %.2f GB available, %.2f GB required", totalGB, minGB) } return result, nil } // CheckCPUCores checks if CPU core count meets minimum requirements func CheckCPUCores(ctx context.Context, executor CommandExecutor, minCores int) (*CheckResult, error) { // Read /proc/cpuinfo to get CPU core count (more universal than nproc) output, err := executor.ExecuteCommand(ctx, "cat", "/proc/cpuinfo") if err != nil { return nil, fmt.Errorf("failed to check CPU cores: %w", err) } // Count the number of "processor" lines cores := 0 lines := strings.Split(output, "\n") for _, line := range lines { if strings.HasPrefix(strings.TrimSpace(line), "processor") { cores++ } } if cores == 0 { return nil, fmt.Errorf("failed to parse CPU cores from /proc/cpuinfo") } result := &CheckResult{ Name: "CPU Cores", Passed: cores >= minCores, Actual: fmt.Sprintf("%d cores", cores), Expected: fmt.Sprintf(">= %d cores", minCores), } if !result.Passed { result.Message = fmt.Sprintf("Insufficient CPU cores: %d cores available, %d cores required", cores, minCores) } return result, nil } // CheckCPUFrequency checks if CPU frequency meets minimum requirements func CheckCPUFrequency(ctx context.Context, executor CommandExecutor, minGHz float64) (*CheckResult, error) { // Read /proc/cpuinfo to get CPU frequency (more universal than lscpu) output, err := executor.ExecuteCommand(ctx, "cat", "/proc/cpuinfo") if err != nil { return nil, fmt.Errorf("failed to check CPU frequency: %w", err) } var maxFreqMHz float64 lines := strings.Split(output, "\n") // Try to parse from "cpu MHz" field (runtime frequency) for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "cpu MHz") { fields := strings.Split(line, ":") if len(fields) >= 2 { freqStr := strings.TrimSpace(fields[1]) freq, err := strconv.ParseFloat(freqStr, 64) if err == nil && freq > maxFreqMHz { maxFreqMHz = freq } } } } // If not found, try to parse from "model name" field (base frequency) if maxFreqMHz == 0 { for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "model name") { // Look for pattern like "@ 2.60GHz" if idx := strings.Index(line, "@"); idx != -1 { freqPart := line[idx+1:] // Extract GHz value if ghzIdx := strings.Index(freqPart, "GHz"); ghzIdx != -1 { freqStr := strings.TrimSpace(freqPart[:ghzIdx]) freqGHz, err := strconv.ParseFloat(freqStr, 64) if err == nil { maxFreqMHz = freqGHz * 1000.0 break } } } } } } if maxFreqMHz == 0 { return nil, fmt.Errorf("failed to parse CPU frequency from /proc/cpuinfo") } freqGHz := maxFreqMHz / 1000.0 minMHz := minGHz * 1000.0 result := &CheckResult{ Name: "CPU Frequency", Passed: maxFreqMHz >= minMHz, Actual: fmt.Sprintf("%.2f GHz", freqGHz), Expected: fmt.Sprintf(">= %.2f GHz", minGHz), } if !result.Passed { result.Message = fmt.Sprintf("Insufficient CPU frequency: %.2f GHz available, %.2f GHz required", freqGHz, minGHz) } return result, nil }