wip: 重新整理 install cmd
This commit is contained in:
74
pkg/syscheck/cpu.go
Normal file
74
pkg/syscheck/cpu.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package syscheck
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/cpu"
|
||||
)
|
||||
|
||||
type CPUInfo struct {
|
||||
Cores int64
|
||||
FrequencyMHz int64
|
||||
SupportAES bool
|
||||
IsX86V2 bool
|
||||
}
|
||||
|
||||
func GetCPUInfo(ctx context.Context) (CPUInfo, error) {
|
||||
info := CPUInfo{
|
||||
Cores: int64(runtime.NumCPU()),
|
||||
}
|
||||
|
||||
// Parse /proc/cpuinfo to get CPU frequency and model info
|
||||
file, err := os.Open("/proc/cpuinfo")
|
||||
if err != nil {
|
||||
return info, fmt.Errorf("failed to open /proc/cpuinfo: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Parse CPU MHz
|
||||
if strings.HasPrefix(line, "cpu MHz") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) == 2 {
|
||||
freqStr := strings.TrimSpace(parts[1])
|
||||
if freq, err := strconv.ParseFloat(freqStr, 64); err == nil {
|
||||
info.FrequencyMHz = int64(freq)
|
||||
break // Get first CPU frequency
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return info, fmt.Errorf("failed to read /proc/cpuinfo: %w", err)
|
||||
}
|
||||
|
||||
// Check CPU features using x/sys/cpu package
|
||||
if runtime.GOARCH == "amd64" || runtime.GOARCH == "386" {
|
||||
// Check AES-NI support
|
||||
info.SupportAES = cpu.X86.HasAES
|
||||
|
||||
// Check x86-64-v2 support
|
||||
// x86-64-v2 requires: SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT
|
||||
info.IsX86V2 = cpu.X86.HasSSE3 &&
|
||||
cpu.X86.HasSSSE3 &&
|
||||
cpu.X86.HasSSE41 &&
|
||||
cpu.X86.HasSSE42 &&
|
||||
cpu.X86.HasPOPCNT
|
||||
} else {
|
||||
// For ARM or other architectures
|
||||
info.SupportAES = false
|
||||
info.IsX86V2 = false
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
44
pkg/syscheck/cpu_test.go
Normal file
44
pkg/syscheck/cpu_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package syscheck
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetCPUInfo(t *testing.T) {
|
||||
info, err := GetCPUInfo(t.Context())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get CPU info: %v", err)
|
||||
}
|
||||
|
||||
// Validate CPU cores
|
||||
if info.Cores <= 0 {
|
||||
t.Errorf("expected CPU cores > 0, got %d", info.Cores)
|
||||
}
|
||||
|
||||
// Validate CPU frequency (should be reasonable, e.g., 500MHz - 10000MHz)
|
||||
if info.FrequencyMHz <= 0 {
|
||||
t.Errorf("expected CPU frequency > 0, got %.2f", info.FrequencyMHz)
|
||||
}
|
||||
if info.FrequencyMHz < 500 || info.FrequencyMHz > 10000 {
|
||||
t.Logf("Warning: CPU frequency seems unusual: %.2f MHz", info.FrequencyMHz)
|
||||
}
|
||||
|
||||
// Log CPU information
|
||||
t.Logf("CPU Cores: %d", info.Cores)
|
||||
t.Logf("CPU Frequency: %.2f MHz (%.2f GHz)", info.FrequencyMHz, info.FrequencyMHz/1000)
|
||||
t.Logf("AES-NI Support: %v", info.SupportAES)
|
||||
t.Logf("x86-64-v2 Compatible: %v", info.IsX86V2)
|
||||
|
||||
// Log feature support
|
||||
if info.SupportAES {
|
||||
t.Log("✓ CPU supports AES-NI hardware acceleration")
|
||||
} else {
|
||||
t.Log("✗ CPU does not support AES-NI")
|
||||
}
|
||||
|
||||
if info.IsX86V2 {
|
||||
t.Log("✓ CPU is x86-64-v2 compatible (SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT)")
|
||||
} else {
|
||||
t.Log("✗ CPU is not x86-64-v2 compatible")
|
||||
}
|
||||
}
|
||||
157
pkg/syscheck/disk.go
Normal file
157
pkg/syscheck/disk.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package syscheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// GetDiskSpace returns available disk space in bytes for the root partition
|
||||
func GetDiskSpace(ctx context.Context) (int64, error) {
|
||||
var stat unix.Statfs_t
|
||||
if err := unix.Statfs("/", &stat); err != nil {
|
||||
return 0, fmt.Errorf("failed to get disk space: %w", err)
|
||||
}
|
||||
|
||||
// Available space = Available blocks * Block size
|
||||
availableSpace := int64(stat.Bavail) * int64(stat.Bsize)
|
||||
return availableSpace, nil
|
||||
}
|
||||
|
||||
// GetDiskSpeed measures disk read/write speed by writing and reading a 1GB test file
|
||||
// Returns read speed and write speed in bytes per second
|
||||
func GetDiskSpeed(ctx context.Context) (int64, int64, error) {
|
||||
const (
|
||||
testSize = 1024 * 1024 * 1024 // 1GB
|
||||
bufferSize = 1024 * 1024 // 1MB buffer
|
||||
)
|
||||
|
||||
tmpFile := "/tmp/diskspeed_test_" + strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
defer func() {
|
||||
// Clean up test file
|
||||
_ = os.Remove(tmpFile)
|
||||
}()
|
||||
|
||||
// Test write speed
|
||||
writeSpeed, err := measureWriteSpeed(ctx, tmpFile, testSize, bufferSize)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to measure write speed: %w", err)
|
||||
}
|
||||
|
||||
// Test read speed
|
||||
readSpeed, err := measureReadSpeed(ctx, tmpFile, bufferSize)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to measure read speed: %w", err)
|
||||
}
|
||||
|
||||
return readSpeed, writeSpeed, nil
|
||||
}
|
||||
|
||||
// measureWriteSpeed writes test data to a file and measures the speed
|
||||
func measureWriteSpeed(ctx context.Context, filename string, totalSize, bufferSize int64) (int64, error) {
|
||||
// Create file
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to create test file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Prepare buffer with random data
|
||||
buffer := make([]byte, bufferSize)
|
||||
if _, err := rand.Read(buffer); err != nil {
|
||||
return 0, fmt.Errorf("failed to generate random data: %w", err)
|
||||
}
|
||||
|
||||
// Start timing
|
||||
startTime := time.Now()
|
||||
var written int64
|
||||
|
||||
// Write data in chunks
|
||||
for written < totalSize {
|
||||
// Check context cancellation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// Write buffer
|
||||
n, err := file.Write(buffer)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to write data: %w", err)
|
||||
}
|
||||
written += int64(n)
|
||||
}
|
||||
|
||||
// Sync to ensure data is written to disk
|
||||
if err := file.Sync(); err != nil {
|
||||
return 0, fmt.Errorf("failed to sync file: %w", err)
|
||||
}
|
||||
|
||||
// Calculate speed
|
||||
duration := time.Since(startTime)
|
||||
speed := int64(float64(written) / duration.Seconds())
|
||||
|
||||
return speed, nil
|
||||
}
|
||||
|
||||
// measureReadSpeed reads the test file and measures the speed
|
||||
func measureReadSpeed(ctx context.Context, filename string, bufferSize int64) (int64, error) {
|
||||
// Open file
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to open test file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Get file size
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to stat file: %w", err)
|
||||
}
|
||||
totalSize := fileInfo.Size()
|
||||
|
||||
// Prepare buffer
|
||||
buffer := make([]byte, bufferSize)
|
||||
|
||||
// Start timing
|
||||
startTime := time.Now()
|
||||
var totalRead int64
|
||||
|
||||
// Read data in chunks
|
||||
for {
|
||||
// Check context cancellation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// Read buffer
|
||||
n, err := file.Read(buffer)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to read data: %w", err)
|
||||
}
|
||||
totalRead += int64(n)
|
||||
}
|
||||
|
||||
// Calculate speed
|
||||
duration := time.Since(startTime)
|
||||
speed := int64(float64(totalRead) / duration.Seconds())
|
||||
|
||||
// Verify we read the entire file
|
||||
if totalRead != totalSize {
|
||||
return 0, fmt.Errorf("read size mismatch: expected %d, got %d", totalSize, totalRead)
|
||||
}
|
||||
|
||||
return speed, nil
|
||||
}
|
||||
42
pkg/syscheck/disk_test.go
Normal file
42
pkg/syscheck/disk_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package syscheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetDiskSpace(t *testing.T) {
|
||||
size, err := GetDiskSpace(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get disk space: %v", err)
|
||||
}
|
||||
|
||||
if size <= 0 {
|
||||
t.Errorf("expected disk space to be greater than 0, got %d", size)
|
||||
}
|
||||
|
||||
t.Logf("Available disk space: %d bytes (%.2f GB)", size, float64(size)/(1024*1024*1024))
|
||||
}
|
||||
|
||||
func TestGetDiskSpeed(t *testing.T) {
|
||||
// Test with real disk I/O (warning: this writes 1GB to disk)
|
||||
// Skip in short mode
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping disk speed test in short mode")
|
||||
}
|
||||
|
||||
rs, ws, err := GetDiskSpeed(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get disk speed: %v", err)
|
||||
}
|
||||
|
||||
if rs <= 0 {
|
||||
t.Errorf("expected read speed > 0, got %d", rs)
|
||||
}
|
||||
if ws <= 0 {
|
||||
t.Errorf("expected write speed > 0, got %d", ws)
|
||||
}
|
||||
|
||||
t.Logf("Read speed: %d bytes/s (%.2f MB/s)", rs, float64(rs)/(1024*1024))
|
||||
t.Logf("Write speed: %d bytes/s (%.2f MB/s)", ws, float64(ws)/(1024*1024))
|
||||
}
|
||||
144
pkg/syscheck/mem.go
Normal file
144
pkg/syscheck/mem.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package syscheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// GetMemorySpace returns total physical memory (RAM) in bytes, excluding swap
|
||||
func GetMemorySpace(ctx context.Context) (int64, error) {
|
||||
var info unix.Sysinfo_t
|
||||
if err := unix.Sysinfo(&info); err != nil {
|
||||
return 0, fmt.Errorf("failed to get memory info: %w", err)
|
||||
}
|
||||
|
||||
// Total physical RAM (excluding swap)
|
||||
// info.Totalram is in memory unit size (info.Unit)
|
||||
totalMemory := int64(info.Totalram) * int64(info.Unit)
|
||||
return totalMemory, nil
|
||||
}
|
||||
|
||||
// GetMemorySpeed measures memory read/write speed by allocating and accessing memory
|
||||
// Returns read speed and write speed in bytes per second
|
||||
func GetMemorySpeed(ctx context.Context) (int64, int64, error) {
|
||||
const (
|
||||
testSize = 512 * 1024 * 1024 // 512MB test size
|
||||
iterations = 5 // Number of iterations for averaging
|
||||
)
|
||||
|
||||
// Test write speed
|
||||
writeSpeed, err := measureMemoryWriteSpeed(ctx, testSize, iterations)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to measure memory write speed: %w", err)
|
||||
}
|
||||
|
||||
// Test read speed
|
||||
readSpeed, err := measureMemoryReadSpeed(ctx, testSize, iterations)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to measure memory read speed: %w", err)
|
||||
}
|
||||
|
||||
return readSpeed, writeSpeed, nil
|
||||
}
|
||||
|
||||
// measureMemoryWriteSpeed measures memory write speed
|
||||
func measureMemoryWriteSpeed(ctx context.Context, size int64, iterations int) (int64, error) {
|
||||
var totalDuration time.Duration
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
// Check context cancellation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// Allocate memory buffer
|
||||
buffer := make([]byte, size)
|
||||
|
||||
// Generate random data
|
||||
source := make([]byte, 1024*1024) // 1MB source buffer
|
||||
if _, err := rand.Read(source); err != nil {
|
||||
return 0, fmt.Errorf("failed to generate random data: %w", err)
|
||||
}
|
||||
|
||||
// Start timing
|
||||
startTime := time.Now()
|
||||
|
||||
// Write data to memory buffer
|
||||
for offset := int64(0); offset < size; offset += int64(len(source)) {
|
||||
remaining := size - offset
|
||||
if remaining < int64(len(source)) {
|
||||
copy(buffer[offset:], source[:remaining])
|
||||
} else {
|
||||
copy(buffer[offset:offset+int64(len(source))], source)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop timing
|
||||
duration := time.Since(startTime)
|
||||
totalDuration += duration
|
||||
|
||||
// Force the buffer to be used to prevent optimization
|
||||
_ = buffer[0]
|
||||
}
|
||||
|
||||
// Calculate average speed
|
||||
avgDuration := totalDuration / time.Duration(iterations)
|
||||
speed := int64(float64(size) / avgDuration.Seconds())
|
||||
|
||||
return speed, nil
|
||||
}
|
||||
|
||||
// measureMemoryReadSpeed measures memory read speed
|
||||
func measureMemoryReadSpeed(ctx context.Context, size int64, iterations int) (int64, error) {
|
||||
var totalDuration time.Duration
|
||||
|
||||
// Pre-allocate and fill buffer
|
||||
buffer := make([]byte, size)
|
||||
if _, err := rand.Read(buffer); err != nil {
|
||||
return 0, fmt.Errorf("failed to initialize buffer: %w", err)
|
||||
}
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
// Check context cancellation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// Start timing
|
||||
startTime := time.Now()
|
||||
|
||||
// Read data from memory buffer
|
||||
var sum int64
|
||||
for offset := int64(0); offset < size; offset += 1024 {
|
||||
// Read in chunks to simulate real access patterns
|
||||
end := offset + 1024
|
||||
if end > size {
|
||||
end = size
|
||||
}
|
||||
for j := offset; j < end; j++ {
|
||||
sum += int64(buffer[j])
|
||||
}
|
||||
}
|
||||
|
||||
// Stop timing
|
||||
duration := time.Since(startTime)
|
||||
totalDuration += duration
|
||||
|
||||
// Use sum to prevent optimization
|
||||
_ = sum
|
||||
}
|
||||
|
||||
// Calculate average speed
|
||||
avgDuration := totalDuration / time.Duration(iterations)
|
||||
speed := int64(float64(size) / avgDuration.Seconds())
|
||||
|
||||
return speed, nil
|
||||
}
|
||||
42
pkg/syscheck/mem_test.go
Normal file
42
pkg/syscheck/mem_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package syscheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetMemorySpace(t *testing.T) {
|
||||
size, err := GetMemorySpace(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get memory space: %v", err)
|
||||
}
|
||||
|
||||
if size <= 0 {
|
||||
t.Errorf("expected memory space to be greater than 0, got %d", size)
|
||||
}
|
||||
|
||||
t.Logf("Total physical memory: %d bytes (%.2f GB)", size, float64(size)/(1024*1024*1024))
|
||||
}
|
||||
|
||||
func TestGetMemorySpeed(t *testing.T) {
|
||||
// Test memory speed (may take a few seconds)
|
||||
// Skip in short mode
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping memory speed test in short mode")
|
||||
}
|
||||
|
||||
rs, ws, err := GetMemorySpeed(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get memory speed: %v", err)
|
||||
}
|
||||
|
||||
if rs <= 0 {
|
||||
t.Errorf("expected read speed > 0, got %d", rs)
|
||||
}
|
||||
if ws <= 0 {
|
||||
t.Errorf("expected write speed > 0, got %d", ws)
|
||||
}
|
||||
|
||||
t.Logf("Memory read speed: %d bytes/s (%.2f GB/s)", rs, float64(rs)/(1024*1024*1024))
|
||||
t.Logf("Memory write speed: %d bytes/s (%.2f GB/s)", ws, float64(ws)/(1024*1024*1024))
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
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
|
||||
}
|
||||
50
pkg/tool/human/size.go
Normal file
50
pkg/tool/human/size.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package human
|
||||
|
||||
import "fmt"
|
||||
|
||||
func Duration(nano int64) string {
|
||||
duration := float64(nano)
|
||||
unit := "ns"
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = "us"
|
||||
}
|
||||
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = "ms"
|
||||
}
|
||||
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = " s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%6.2f%s", duration, unit)
|
||||
}
|
||||
|
||||
func Size(size int64) string {
|
||||
const (
|
||||
_ = iota
|
||||
KB = 1 << (10 * iota) // 1 KB = 1024 bytes
|
||||
MB // 1 MB = 1024 KB
|
||||
GB // 1 GB = 1024 MB
|
||||
TB // 1 TB = 1024 GB
|
||||
PB // 1 PB = 1024 TB
|
||||
)
|
||||
|
||||
switch {
|
||||
case size >= PB:
|
||||
return fmt.Sprintf("%.2f PB", float64(size)/PB)
|
||||
case size >= TB:
|
||||
return fmt.Sprintf("%.2f TB", float64(size)/TB)
|
||||
case size >= GB:
|
||||
return fmt.Sprintf("%.2f GB", float64(size)/GB)
|
||||
case size >= MB:
|
||||
return fmt.Sprintf("%.2f MB", float64(size)/MB)
|
||||
case size >= KB:
|
||||
return fmt.Sprintf("%.2f KB", float64(size)/KB)
|
||||
default:
|
||||
return fmt.Sprintf("%d bytes", size)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user