319 lines
9.2 KiB
Go
319 lines
9.2 KiB
Go
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
|
|
}
|