diff --git a/internal/cmd/installcmd/check.go b/internal/cmd/installcmd/check.go index 8d2bac9..abf5c54 100644 --- a/internal/cmd/installcmd/check.go +++ b/internal/cmd/installcmd/check.go @@ -10,7 +10,6 @@ import ( func Check() *cobra.Command { var ( workdir string - target string ignoreDisk bool ignoreMemory bool ignoreCPU bool @@ -32,8 +31,8 @@ func Check() *cobra.Command { } }, RunE: func(cmd *cobra.Command, args []string) error { - _installer := installer.NewInstaller(workdir, target) - return _installer.Check( + _installer := installer.NewInstaller(workdir) + return _installer.HardwareCheck( cmd.Context(), installer.WithIgnoreDiskCheck(ignoreDisk), installer.WithIgnoreMemoryCheck(ignoreMemory), @@ -43,7 +42,6 @@ func Check() *cobra.Command { } _cmd.Flags().StringVar(&workdir, "workdir", "/root/hs-installation", "Working directory") - _cmd.Flags().StringVar(&target, "target", "self", "Target") return _cmd } diff --git a/internal/cmd/installcmd/k0s.go b/internal/cmd/installcmd/k0s.go index 46cb8bb..2b0d209 100644 --- a/internal/cmd/installcmd/k0s.go +++ b/internal/cmd/installcmd/k0s.go @@ -9,21 +9,23 @@ func K0s() *cobra.Command { var ( workdir string - target string ) _cmd := &cobra.Command{ Use: "k0s", Short: "Install k0s", Long: "Install k0s", + PreRunE: func(cmd *cobra.Command, args []string) error { + i := installer.NewInstaller(workdir) + return i.CheckOK(cmd.Context()) + }, RunE: func(cmd *cobra.Command, args []string) error { - _installer := installer.NewInstaller(workdir, target) + _installer := installer.NewInstaller(workdir) return _installer.K0s(cmd.Context()) }, } _cmd.PersistentFlags().StringVar(&workdir, "workdir", "/root/hs-installation", "working directory") - _cmd.PersistentFlags().StringVar(&target, "target", "self", "target directory") return _cmd } diff --git a/internal/cmd/installcmd/prepare.go b/internal/cmd/installcmd/prepare.go index 3dd35ef..dbb335f 100644 --- a/internal/cmd/installcmd/prepare.go +++ b/internal/cmd/installcmd/prepare.go @@ -9,22 +9,24 @@ func Prepare() *cobra.Command { var ( workdir string - target string ) _cmd := &cobra.Command{ Use: "prepare", Short: "Prepare for installation", Long: "Prepare for installation", + PreRunE: func(cmd *cobra.Command, args []string) error { + i := installer.NewInstaller(workdir) + return i.CheckOK(cmd.Context()) + }, RunE: func(cmd *cobra.Command, args []string) error { - _installer := installer.NewInstaller(workdir, target) + _installer := installer.NewInstaller(workdir) _installer.Prepare(cmd.Context()) return nil }, } _cmd.Flags().StringVar(&workdir, "workdir", "/root/hs-installation", "Working directory") - _cmd.Flags().StringVar(&target, "target", "self", "Target") return _cmd } diff --git a/internal/controller/installer/installer.check.go b/internal/controller/installer/installer.check.go index e9ae639..84838e4 100644 --- a/internal/controller/installer/installer.check.go +++ b/internal/controller/installer/installer.check.go @@ -1,11 +1,18 @@ package installer import ( + "bufio" "context" "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "time" "yizhisec.com/hsv2/forge/pkg/logger" "yizhisec.com/hsv2/forge/pkg/syscheck" + "yizhisec.com/hsv2/forge/pkg/tool/human" ) type CheckOption func(*checkOpt) @@ -13,6 +20,7 @@ type checkOpt struct { ignoreDisk bool ignoreMemory bool ignoreCPU bool + noWriteDown bool } func WithIgnoreDiskCheck(ignore bool) CheckOption { @@ -33,9 +41,177 @@ func WithIgnoreCPUCheck(ignore bool) CheckOption { } } -func (i *installer) Check(ctx context.Context, opts ...CheckOption) error { +func WithNoWriteDown(noWriteDown bool) CheckOption { + return func(o *checkOpt) { + o.noWriteDown = noWriteDown + } +} + +type HardwareCheckResult struct { + Timestamp int64 // ms + DiskSize int64 // bytes, if err, -1; if ignore, 0 + DiskReadSpeed int64 // bytes/s, if err, -1; if ignore, 0 + DiskWriteSpeed int64 // bytes/s, if err, -1; if ignore, 0 + MemorySize int64 // bytes, if err, -1; if ignore, 0 + MemoryReadSpeed int64 // bytes/s, if err, -1; if ignore, 0 + MemoryWriteSpeed int64 // bytes/s, if err, -1; if ignore, 0 + CPUCores int64 // if err, -1; if ignore, 0 + CPUFrequency int64 // MHz, if err, -1; if ignore, 0 + CPUSupportAES bool + CPUIsX86V2 bool +} + +func (h *HardwareCheckResult) Write(filename string) error { + content := []byte(fmt.Sprintf(`timestamp=%d +disk_size=%d +disk_read_speed=%d +disk_write_speed=%d +memory_size=%d +memory_read_speed=%d +memory_write_speed=%d +cpu_cores=%d +cpu_frequency=%d +cpu_support_aes=%t +cpu_is_x86_v2=%t`, + h.Timestamp, + h.DiskSize, + h.DiskReadSpeed, + h.DiskWriteSpeed, + h.MemorySize, + h.MemoryReadSpeed, + h.MemoryWriteSpeed, + h.CPUCores, + h.CPUFrequency, + h.CPUSupportAES, + h.CPUIsX86V2, + )) + return os.WriteFile(filename, content, 0644) +} + +func (h *HardwareCheckResult) Load(filename string) error { + file, err := os.Open(filename) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + switch key { + case "timestamp": + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + h.Timestamp = -1 + } else { + h.Timestamp = v + } + case "disk_size": + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + h.DiskSize = -1 + } else { + h.DiskSize = v + } + case "disk_read_speed": + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + h.DiskReadSpeed = -1 + } else { + h.DiskReadSpeed = v + } + case "disk_write_speed": + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + h.DiskWriteSpeed = -1 + } else { + h.DiskWriteSpeed = v + } + case "memory_size": + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + h.MemorySize = -1 + } else { + h.MemorySize = v + } + case "memory_read_speed": + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + h.MemoryReadSpeed = -1 + } else { + h.MemoryReadSpeed = v + } + case "memory_write_speed": + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + h.MemoryWriteSpeed = -1 + } else { + h.MemoryWriteSpeed = v + } + case "cpu_cores": + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + h.CPUCores = -1 + } else { + h.CPUCores = v + } + case "cpu_frequency": + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + h.CPUFrequency = -1 + } else { + h.CPUFrequency = v + } + case "cpu_support_aes": + v, err := strconv.ParseBool(value) + if err == nil { + h.CPUSupportAES = v + } + // bool fields don't use -1, keep default false on error + case "cpu_is_x86_v2": + v, err := strconv.ParseBool(value) + if err == nil { + h.CPUIsX86V2 = v + } + // bool fields don't use -1, keep default false on error + } + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + + return nil +} + +func (h *HardwareCheckResult) Pass(opts ...CheckOption) error { + const ( + DISK_SIZE int64 = 480 * 1024 * 1024 * 1024 // 480GB + DISK_READ_SPEED int64 = 1024 * 1024 * 1024 // 1024MB/s + DISK_WRITE_SPEED int64 = 500 * 1024 * 1024 // 500MB/s + MEMORY_SIZE int64 = 15.5 * 1024 * 1024 * 1024 // 15.5GB + MEMORY_READ_SPEED int64 = 1024 * 1024 * 1024 // 1024MB/s + MEMORY_WRITE_SPEED int64 = 500 * 1024 * 1024 // 500MB/s + CPUCORES int64 = 8 + CPU_FREQUENCY int64 = 2.0 * 1024 // 2.0GHz + CPU_NEED_V2 = true + CPU_NEED_AES = true + ) + var ( - err error + now = time.Now() o = &checkOpt{} ) @@ -43,98 +219,195 @@ func (i *installer) Check(ctx context.Context, opts ...CheckOption) error { fn(o) } - logger.Info("☑️ installer.Check: Starting system checks...") - - if err = i.targetOK(ctx); err != nil { - return err + if h.Timestamp < now.AddDate(0, 0, -1).UnixMilli() { + return fmt.Errorf("上次检测结果已失效, 请重新检测") } - // 1. Check disk space: >= 500GB - logger.Info("☑️ installer.Check: Checking disk space...") - diskSpaceResult, err := syscheck.CheckDiskSpace(ctx, i, 500) - if err != nil { - logger.Debug("❌ installer.Check: Failed to check disk space: %v", err) - return fmt.Errorf("failed to check disk space: %w", err) + if o.ignoreDisk || h.DiskSize+h.DiskReadSpeed+h.DiskWriteSpeed == -3 { + goto PASS_MEMORY } - if !diskSpaceResult.Passed { - logger.Error("❌ %s: %s (Expected: %s, Actual: %s)", diskSpaceResult.Name, diskSpaceResult.Message, diskSpaceResult.Expected, diskSpaceResult.Actual) - if !o.ignoreDisk { - return fmt.Errorf("disk space check failed: %s", diskSpaceResult.Message) - } + if h.DiskSize < DISK_SIZE { + return fmt.Errorf("磁盘大小不满足, 需要: %s, 实际: %s", human.Size(DISK_SIZE), human.Size(h.DiskSize)) } - logger.Info("✅ %s: %s", diskSpaceResult.Name, diskSpaceResult.Actual) - - // 2. Check disk performance: write >= 500MB/s, read >= 500MB/s - logger.Info("☑️ installer.Check: Checking disk performance...") - diskPerfResult, err := syscheck.CheckDiskPerformance(ctx, i, 500, 500) - if err != nil { - logger.Debug("❌ installer.Check: Failed to check disk performance: %v", err) - return fmt.Errorf("failed to check disk performance: %w", err) + if h.DiskReadSpeed < DISK_READ_SPEED { + return fmt.Errorf("磁盘读速度不满足, 需要: %s, 实际: %s", human.Size(DISK_READ_SPEED), human.Size(h.DiskReadSpeed)) } - if !diskPerfResult.Passed { - logger.Error("❌ %s: %s (Expected: %s, Actual: %s)", diskPerfResult.Name, diskPerfResult.Message, diskPerfResult.Expected, diskPerfResult.Actual) - if !o.ignoreDisk { - return fmt.Errorf("disk performance check failed: %s", diskPerfResult.Message) - } + if h.DiskWriteSpeed < DISK_WRITE_SPEED { + return fmt.Errorf("磁盘写速度不满足, 需要: %s, 实际: %s", human.Size(DISK_WRITE_SPEED), human.Size(h.DiskWriteSpeed)) } - logger.Info("✅ %s: %s", diskPerfResult.Name, diskPerfResult.Actual) +PASS_MEMORY: - // 3. Check memory size: >= 15.5GB - logger.Info("☑️ installer.Check: Checking memory size...") - memResult, err := syscheck.CheckMemory(ctx, i, 15.5) - if err != nil { - logger.Debug("❌ installer.Check: Failed to check memory: %v", err) - return fmt.Errorf("failed to check memory: %w", err) + if o.ignoreMemory || h.MemorySize+h.MemoryReadSpeed+h.MemoryWriteSpeed == -3 { + goto PASS_CPU } - if !memResult.Passed { - logger.Error("❌ %s: %s (Expected: %s, Actual: %s)", memResult.Name, memResult.Message, memResult.Expected, memResult.Actual) - if !o.ignoreMemory { - return fmt.Errorf("memory check failed: %s", memResult.Message) - } + if h.MemorySize < MEMORY_SIZE { + return fmt.Errorf("内存大小不满足, 需要: %s, 实际: %s", human.Size(MEMORY_SIZE), human.Size(h.MemorySize)) } - logger.Info("✅ %s: %s", memResult.Name, memResult.Actual) - - // 4. Check CPU cores: >= 8 - logger.Info("☑️ installer.Check: Checking CPU cores...") - cpuCoresResult, err := syscheck.CheckCPUCores(ctx, i, 8) - if err != nil { - logger.Debug("❌ installer.Check: Failed to check CPU cores: %v", err) - return fmt.Errorf("failed to check CPU cores: %w", err) + if h.MemoryReadSpeed < MEMORY_READ_SPEED { + return fmt.Errorf("内存读速度不满足, 需要: %s, 实际: %s", human.Size(MEMORY_READ_SPEED), human.Size(h.MemoryReadSpeed)) } - if !cpuCoresResult.Passed { - logger.Error("❌ %s: %s (Expected: %s, Actual: %s)", cpuCoresResult.Name, cpuCoresResult.Message, cpuCoresResult.Expected, cpuCoresResult.Actual) - if !o.ignoreCPU { - return fmt.Errorf("CPU cores check failed: %s", cpuCoresResult.Message) - } + if h.MemoryWriteSpeed < MEMORY_WRITE_SPEED { + return fmt.Errorf("内存写速度不满足, 需要: %s, 实际: %s", human.Size(MEMORY_WRITE_SPEED), human.Size(h.MemoryWriteSpeed)) } - logger.Info("✅ %s: %s", cpuCoresResult.Name, cpuCoresResult.Actual) +PASS_CPU: - // 5. Check CPU frequency: >= 2GHz - logger.Info("☑️ installer.Check: Checking CPU frequency...") - cpuFreqResult, err := syscheck.CheckCPUFrequency(ctx, i, 2.0) - if err != nil { - logger.Debug("❌ installer.Check: Failed to check CPU frequency: %v", err) - return fmt.Errorf("failed to check CPU frequency: %w", err) + if o.ignoreCPU || h.CPUCores+h.CPUFrequency == -2 { + goto END + } + if h.CPUCores < CPUCORES { + return fmt.Errorf("cpu 核心数不满足, 需要: %d, 实际: %d", CPUCORES, h.CPUCores) } - if !cpuFreqResult.Passed { - logger.Error("❌ %s: %s (Expected: %s, Actual: %s)", cpuFreqResult.Name, cpuFreqResult.Message, cpuFreqResult.Expected, cpuFreqResult.Actual) - if !o.ignoreCPU { - return fmt.Errorf("CPU frequency check failed: %s", cpuFreqResult.Message) - } + if h.CPUFrequency < CPU_FREQUENCY { + return fmt.Errorf("cpu 频率不满足, 需要: %s, 实际: %s", human.Size(CPU_FREQUENCY), human.Size(h.CPUFrequency)) } - logger.Info("✅ %s: %s", cpuFreqResult.Name, cpuFreqResult.Actual) + if CPU_NEED_V2 && !h.CPUIsX86V2 { + return fmt.Errorf("cpu 不支持aes") + } - logger.Info("✅ installer.Check: All system checks passed successfully!") + if CPU_NEED_AES && !h.CPUSupportAES { + return fmt.Errorf("cpu 不支持 x86_v2s") + } + +END: return nil } + +func (i *installer) HardwareCheck(ctx context.Context, opts ...CheckOption) error { + var ( + err error + o = &checkOpt{} + now = time.Now() + result = &HardwareCheckResult{ + Timestamp: now.UnixMilli(), + } + cpuInfo syscheck.CPUInfo + dir string + ) + + for _, fn := range opts { + fn(o) + } + + logger.Info("✅ 开始目标机器系统检测...") + + if o.ignoreDisk { + logger.Warn("⚠️ 跳过磁盘检测") + result.DiskSize = -1 + result.DiskReadSpeed = -1 + result.DiskWriteSpeed = -1 + goto CHECK_MEMORY + } + + if result.DiskSize, err = syscheck.GetDiskSpace(ctx); err != nil { + logger.Debug("❌ installer.HardwareCheck: Failed to get disk space: %v", err) + return err + } + + logger.Info("💾 磁盘容量: %s", human.Size(result.DiskSize)) + + if result.DiskReadSpeed, result.DiskWriteSpeed, err = syscheck.GetDiskSpeed(ctx); err != nil { + logger.Debug("❌ installer.HardwareCheck: Failed to get disk speed: %v", err) + return err + } + + logger.Info("💾 磁盘速率: 读取(%s/每秒), 写入(%s/每秒)", human.Size(result.DiskReadSpeed), human.Size(result.DiskWriteSpeed)) + +CHECK_MEMORY: + + if o.ignoreMemory { + logger.Warn("⚠️ 跳过内存检测") + result.MemorySize = -1 + result.MemoryReadSpeed = -1 + result.MemoryWriteSpeed = -1 + goto CHECK_CPU + } + + if result.MemorySize, err = syscheck.GetMemorySpace(ctx); err != nil { + logger.Debug("❌ installer.HardwareCheck: Failed to get memory size: %v", err) + return err + } + + logger.Info("💿 内存容量: %s", human.Size(result.MemorySize)) + + if result.MemoryReadSpeed, result.MemoryWriteSpeed, err = syscheck.GetMemorySpeed(ctx); err != nil { + logger.Debug("❌ installer.HardwareCheck: Failed to get memory speed: %v", err) + return err + } + + logger.Info("💿 内存速率: 读取(%s/每秒), 写入(%s/每秒)", human.Size(result.MemoryReadSpeed), human.Size(result.MemoryWriteSpeed)) + +CHECK_CPU: + if o.ignoreCPU { + logger.Warn("⚠️ 跳过 CPU 检测") + result.CPUCores = -1 + result.CPUFrequency = -1 + result.CPUSupportAES = false + result.CPUIsX86V2 = false + goto END + } + + if cpuInfo, err = syscheck.GetCPUInfo(ctx); err != nil { + logger.Debug("❌ installer.HardwareCheck: Failed to get CPU info: %v", err) + return err + } + + result.CPUCores = cpuInfo.Cores + result.CPUFrequency = cpuInfo.FrequencyMHz + result.CPUSupportAES = cpuInfo.SupportAES + result.CPUIsX86V2 = cpuInfo.IsX86V2 + + logger.Info("🧮 CPU 核心数: %d", result.CPUCores) + logger.Info("🧮 CPU 频率: %d Mhz", result.CPUFrequency) + logger.Info("🧮 CPU 支持 AES: %t", result.CPUSupportAES) +END: + + if dir, err = os.UserHomeDir(); err != nil { + logger.Debug("❌ installer.HardwareCheck: Failed to get user home directory: %v", err) + return err + } + + if err = result.Write(filepath.Join(dir, ".hsv2-installation")); err != nil { + logger.Debug("❌ installer.HardwareCheck: Failed to write installation file: %v", err) + return err + } + + if err = result.Pass(opts...); err != nil { + logger.Error("❌ %s", err.Error()) + return nil + } + + logger.Info("✅ 检测完成") + + return nil +} + +func (*installer) CheckOK(ctx context.Context) error { + var ( + err error + h = &HardwareCheckResult{} + dir string + ) + + if dir, err = os.UserHomeDir(); err != nil { + logger.Debug("❌ installer.HardwareCheck: Failed to get user home directory: %v", err) + return err + } + + if err = h.Load(filepath.Join(dir, ".hsv2-installation")); err != nil { + logger.Debug("❌ installer.HardwareCheck: Failed to load installation file: %v", err) + return err + } + + return h.Pass() +} diff --git a/internal/controller/installer/installer.go b/internal/controller/installer/installer.go index dfbef05..42df8f5 100644 --- a/internal/controller/installer/installer.go +++ b/internal/controller/installer/installer.go @@ -2,41 +2,27 @@ package installer import ( "context" - "errors" "fmt" - "io" - "os" "os/exec" - "path/filepath" + "strings" - "yizhisec.com/hsv2/forge/pkg/logger" + "github.com/samber/lo" ) type installer struct { workdir string - target string -} - -func (i *installer) buildCommand(ctx context.Context, cmds ...string) *exec.Cmd { - if len(cmds) == 0 { - return nil - } - - if i.target == "self" { - return exec.CommandContext(ctx, cmds[0], cmds[1:]...) - } - - sshArgs := append([]string{i.target}, cmds...) - return exec.CommandContext(ctx, "ssh", sshArgs...) } // ExecuteCommand implements syscheck.CommandExecutor interface func (i *installer) ExecuteCommand(ctx context.Context, cmds ...string) (string, error) { - cmd := i.buildCommand(ctx, cmds...) - if cmd == nil { - return "", fmt.Errorf("failed to build command") + fcs := lo.Filter(cmds, func(item string, _ int) bool { return item != "" }) + + if len(fcs) == 0 { + return "", fmt.Errorf("empty commands") } + cmd := exec.CommandContext(ctx, "sh", "-c", strings.Join(fcs, " ")) + output, err := cmd.CombinedOutput() if err != nil { return "", fmt.Errorf("command failed: %w, output: %s", err, string(output)) @@ -45,109 +31,6 @@ func (i *installer) ExecuteCommand(ctx context.Context, cmds ...string) (string, return string(output), nil } -type CopyFileOption func(*copyFileOptions) -type copyFileOptions struct { - addExecutable bool -} - -func withCopyFileExecutable() func(*copyFileOptions) { - return func(o *copyFileOptions) { - o.addExecutable = true - } -} - -func (i *installer) copyFile(ctx context.Context, src, dst string, opts ...CopyFileOption) error { - logger.Debug("☑️ installer.copyFile: Copying file from %s to %s (target: %s)", src, dst, i.target) - - var ( - err error - o = ©FileOptions{} - srcFile, dstFile *os.File - srcInfo os.FileInfo - ) - - for _, fn := range opts { - fn(o) - } - - if i.target == "self" { - // Simply copy file locally - logger.Debug("Copying file locally: %s -> %s", src, dst) - - // Open source file - if srcFile, err = os.Open(src); err != nil { - logger.Error("❌ Failed to open source file %s: %v", src, err) - return fmt.Errorf("failed to open source file: %w", err) - } - defer srcFile.Close() - - // Create destination directory if needed - dstDir := filepath.Dir(dst) - if err = os.MkdirAll(dstDir, 0755); err != nil { - logger.Error("❌ Failed to create destination directory %s: %v", dstDir, err) - return fmt.Errorf("failed to create destination directory: %w", err) - } - - // Create destination file - if dstFile, err = os.Create(dst); err != nil { - logger.Error("❌ Failed to create destination file %s: %v", dst, err) - return fmt.Errorf("failed to create destination file: %w", err) - } - defer dstFile.Close() - - // Copy file content - if _, err = io.Copy(dstFile, srcFile); err != nil { - logger.Error("❌ Failed to copy file content: %v", err) - return fmt.Errorf("failed to copy file content: %w", err) - } - - // Get source file permissions - if srcInfo, err = os.Stat(src); err == nil { - if err = os.Chmod(dst, srcInfo.Mode()); err != nil { - logger.Debug("⚠️ Failed to set file permissions: %v", err) - } - } - - logger.Info("✅ File copied locally: %s -> %s", src, dst) - return nil - } - - // Copy file via scp to remote target - logger.Debug("Copying file via scp: %s -> %s:%s", src, i.target, dst) - - // Format: scp : - cmd := exec.CommandContext(ctx, "scp", src, fmt.Sprintf("%s:%s", i.target, dst)) - output, err := cmd.CombinedOutput() - if err != nil { - logger.Error("❌ Failed to copy file via scp: %v, output: %s", err, string(output)) - return fmt.Errorf("failed to copy file via scp: %w, output: %s", err, string(output)) - } - - logger.Info("✅ File copied via scp: %s -> %s:%s", src, i.target, dst) - return nil -} - -func (i *installer) targetOK(ctx context.Context) error { - cmd := i.buildCommand(ctx, "whoami") - output, err := cmd.CombinedOutput() - if err != nil { - logger.Debug("❌ installer.targetOK: check target %s failed, err = %v", i.target, err) - return err - } - - if string(output) != "root\n" { - logger.Debug("❌ installer.targetOK: check target %s failed, output = %s", i.target, string(output)) - return errors.New("target is not root user") - } - - return nil -} - -func NewInstaller(workdir, target string) *installer { - if target == "" { - logger.Warn("🎯 NewInstaller: target empty, set to default(self)") - target = "self" - } - - return &installer{workdir: workdir, target: target} +func NewInstaller(workdir string) *installer { + return &installer{workdir: workdir} } diff --git a/internal/controller/installer/installer.k0s.go b/internal/controller/installer/installer.k0s.go index b22ef3b..405f955 100644 --- a/internal/controller/installer/installer.k0s.go +++ b/internal/controller/installer/installer.k0s.go @@ -2,12 +2,8 @@ package installer import ( "context" - "fmt" - "os" - "path/filepath" "github.com/samber/lo" - "yizhisec.com/hsv2/forge/pkg/logger" ) type K0sOpt func(*k0sOpt) @@ -41,83 +37,5 @@ func WithK0sWorkerTokenFile(filename string) K0sOpt { } func (i *installer) K0s(ctx context.Context, opts ...K0sOpt) error { - var ( - err error - o = &k0sOpt{ - Type: "controller", - controllerAsWorker: false, - WorkerTokenFile: "/etc/k0s/worker.token", - } - ) - - if err = i.targetOK(ctx); err != nil { - return err - } - - for _, fn := range opts { - fn(o) - } - - binaries := []string{ - "dependency/bin/k0s", - "dependency/bin/k9s", "dependency/bin/kubectl", "dependency/bin/helm"} - if err = i.checkFiles(binaries...); err != nil { - return err - } - - // check image tar files: - images := []string{ - "dependency/image/k0s.apiserver-network-proxy-agent.tar", - "dependency/image/k0s.cni-node.tar", - "dependency/image/k0s.coredns.tar", - "dependency/image/k0s.kube-proxy.tar", - "dependency/image/k0s.kube-router.tar", - "dependency/image/k0s.metrics-server.tar", - "dependency/image/k0s.pause.tar", - } - - if err = i.checkFiles(images...); err != nil { - return err - } - - // copy binaries to /usr/local/bin and add executable permissions - if err = i.copyFile(ctx, "dependency/bin/k0s", "/usr/local/bin/k0s", withCopyFileExecutable()); err != nil { - return err - } - if err = i.copyFile(ctx, "dependency/bin/k9s", "/usr/local/bin/k9s", withCopyFileExecutable()); err != nil { - return err - } - if err = i.copyFile(ctx, "dependency/bin/kubectl", "/usr/local/bin/kubectl", withCopyFileExecutable()); err != nil { - return err - } - if err = i.copyFile(ctx, "dependency/bin/helm", "/usr/local/bin/helm", withCopyFileExecutable()); err != nil { - return err - } - - i.ExecuteCommand(ctx, "k0s", "") - - return nil -} - -// checkBinaryFiles checks if the required binary files exist in the dependency/bin directory -func (i *installer) checkFiles(fileBaseName ...string) error { - logger.Info("☑️ installer.checkFiles: Checking files in %s...", i.workdir) - - for _, file := range fileBaseName { - filename := filepath.Join(i.workdir, file) - logger.Debug("Checking file: %s", filename) - - if _, err := os.Stat(filename); os.IsNotExist(err) { - logger.Error("❌ File not found: %s", filename) - return fmt.Errorf("file not found: %s", filename) - } else if err != nil { - logger.Error("❌ Failed to check file %s: %v", filename, err) - return fmt.Errorf("failed to check file %s: %w", filename, err) - } - - logger.Info("✅ File found: %s", file) - } - - logger.Info("✅ installer.checkBinaryFiles: All binary files verified successfully!") - return nil + panic("plz impl") } diff --git a/internal/controller/installer/installer.prepare.go b/internal/controller/installer/installer.prepare.go index f2bfde6..1b74245 100644 --- a/internal/controller/installer/installer.prepare.go +++ b/internal/controller/installer/installer.prepare.go @@ -14,10 +14,6 @@ func (i *installer) Prepare(ctx context.Context) error { logger.Info("☑️ installer.Prepare: Starting system preparation...") - if err = i.targetOK(ctx); err != nil { - return err - } - // 1. Set timezone to Asia/Shanghai logger.Info("☑️ installer.Prepare: Setting timezone to Asia/Shanghai...") if err = i.setTimezone(ctx); err != nil { @@ -65,31 +61,23 @@ func (i *installer) Prepare(ctx context.Context) error { // setTimezone sets the system timezone to Asia/Shanghai func (i *installer) setTimezone(ctx context.Context) error { + var ( + err error + output string + ) // Check if timezone file exists - cmd := i.buildCommand(ctx, "test", "-f", "/usr/share/zoneinfo/Asia/Shanghai") - if cmd == nil { - return fmt.Errorf("failed to build command") - } - if err := cmd.Run(); err != nil { - return fmt.Errorf("timezone file /usr/share/zoneinfo/Asia/Shanghai not found") + if output, err = i.ExecuteCommand(ctx, "test", "-f", "/usr/share/zoneinfo/Asia/Shanghai"); err != nil { + return fmt.Errorf("failed to set timezone, err =%s, raw = %s", err.Error(), output) } // Remove old localtime link/file - cmd = i.buildCommand(ctx, "rm", "-f", "/etc/localtime") - if cmd == nil { - return fmt.Errorf("failed to build command") - } - if err := cmd.Run(); err != nil { - logger.Debug("Failed to remove /etc/localtime: %v", err) + if output, err = i.ExecuteCommand(ctx, "rm", "-f", "/etc/localtime"); err != nil { + return fmt.Errorf("failed to set timezone, err =%s, raw = %s", err.Error(), output) } // Create symlink - cmd = i.buildCommand(ctx, "ln", "-s", "/usr/share/zoneinfo/Asia/Shanghai", "/etc/localtime") - if cmd == nil { - return fmt.Errorf("failed to build command") - } - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to create symlink: %w", err) + if output, err = i.ExecuteCommand(ctx, "ln", "-s", "/usr/share/zoneinfo/Asia/Shanghai", "/etc/localtime"); err != nil { + return fmt.Errorf("failed to set timezone, err =%s, raw = %s", err.Error(), output) } return nil @@ -97,22 +85,19 @@ func (i *installer) setTimezone(ctx context.Context) error { // disableSwap disables all swap partitions and removes swap entries from /etc/fstab func (i *installer) disableSwap(ctx context.Context) error { + var ( + err error + output string + ) + // Turn off all swap - cmd := i.buildCommand(ctx, "swapoff", "-a") - if cmd == nil { - return fmt.Errorf("failed to build command") - } - if err := cmd.Run(); err != nil { - logger.Debug("Failed to swapoff: %v (may be already off)", err) + if output, err = i.ExecuteCommand(ctx, "swapoff", "-a"); err != nil { + logger.Debug("Failed to swapoff: %v (may be already off), raw = %s", err, output) } // Comment out swap entries in /etc/fstab to make it persistent - cmd = i.buildCommand(ctx, "sed", "-i", "/swap/s/^/#/", "/etc/fstab") - if cmd == nil { - return fmt.Errorf("failed to build command") - } - if err := cmd.Run(); err != nil { - logger.Debug("Failed to comment swap in /etc/fstab: %v", err) + if output, err = i.ExecuteCommand(ctx, "sed", "-i", "/swap/s/^/#/", "/etc/fstab"); err != nil { + logger.Debug("Failed to comment swap in /etc/fstab: %v, raw = %s", err, output) } return nil @@ -120,23 +105,21 @@ func (i *installer) disableSwap(ctx context.Context) error { // loadKernelModule loads a kernel module and ensures it's loaded on boot func (i *installer) loadKernelModule(ctx context.Context, moduleName string) error { + var ( + err error + output string + ) + // Load the module immediately - cmd := i.buildCommand(ctx, "modprobe", moduleName) - if cmd == nil { - return fmt.Errorf("failed to build command") - } - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to load module %s: %w", moduleName, err) + if output, err = i.ExecuteCommand(ctx, "modprobe", moduleName); err != nil { + return fmt.Errorf("failed to load module %s: %w, raw = %s", moduleName, err, output) } // Add to /etc/modules-load.d/ to load on boot filePath := fmt.Sprintf("/etc/modules-load.d/%s.conf", moduleName) - cmd = i.buildCommand(ctx, "bash", "-c", fmt.Sprintf("echo '%s' > %s", moduleName, filePath)) - if cmd == nil { - return fmt.Errorf("failed to build command") - } - if err := cmd.Run(); err != nil { - logger.Debug("Failed to add module to modules-load.d: %v", err) + command := fmt.Sprintf("echo '%s' > %s", moduleName, filePath) + if output, err = i.ExecuteCommand(ctx, "bash", "-c", command); err != nil { + logger.Debug("Failed to add module to modules-load.d: %v, raw = %s", err, output) } return nil @@ -144,6 +127,11 @@ func (i *installer) loadKernelModule(ctx context.Context, moduleName string) err // applySysctlSettings applies required sysctl settings for Kubernetes func (i *installer) applySysctlSettings(ctx context.Context) error { + var ( + err error + output string + ) + const sysctlConfig = `# Kubernetes required settings net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 @@ -163,21 +151,14 @@ net.ipv4.neigh.default.gc_thresh3 = 8192 ` // Write sysctl config file - cmd := i.buildCommand(ctx, "bash", "-c", fmt.Sprintf("cat > /etc/sysctl.d/99-kubernetes.conf << 'EOF'\n%sEOF", sysctlConfig)) - if cmd == nil { - return fmt.Errorf("failed to build command") - } - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to write sysctl config: %w", err) + command := fmt.Sprintf("cat > /etc/sysctl.d/99-kubernetes.conf << 'EOF'\n%sEOF", sysctlConfig) + if output, err = i.ExecuteCommand(ctx, "bash", "-c", command); err != nil { + return fmt.Errorf("failed to write sysctl config: %w, raw = %s", err, output) } // Apply sysctl settings - cmd = i.buildCommand(ctx, "sysctl", "--system") - if cmd == nil { - return fmt.Errorf("failed to build command") - } - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to apply sysctl settings: %w", err) + if output, err = i.ExecuteCommand(ctx, "sysctl", "--system"); err != nil { + return fmt.Errorf("failed to apply sysctl settings: %w, raw = %s", err, output) } return nil diff --git a/pkg/syscheck/cpu.go b/pkg/syscheck/cpu.go new file mode 100644 index 0000000..bc08c3f --- /dev/null +++ b/pkg/syscheck/cpu.go @@ -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 +} diff --git a/pkg/syscheck/cpu_test.go b/pkg/syscheck/cpu_test.go new file mode 100644 index 0000000..31234fc --- /dev/null +++ b/pkg/syscheck/cpu_test.go @@ -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") + } +} diff --git a/pkg/syscheck/disk.go b/pkg/syscheck/disk.go new file mode 100644 index 0000000..3c3cad9 --- /dev/null +++ b/pkg/syscheck/disk.go @@ -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 +} diff --git a/pkg/syscheck/disk_test.go b/pkg/syscheck/disk_test.go new file mode 100644 index 0000000..649a6ef --- /dev/null +++ b/pkg/syscheck/disk_test.go @@ -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)) +} diff --git a/pkg/syscheck/mem.go b/pkg/syscheck/mem.go new file mode 100644 index 0000000..9d64e04 --- /dev/null +++ b/pkg/syscheck/mem.go @@ -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 +} diff --git a/pkg/syscheck/mem_test.go b/pkg/syscheck/mem_test.go new file mode 100644 index 0000000..a9c146f --- /dev/null +++ b/pkg/syscheck/mem_test.go @@ -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)) +} diff --git a/pkg/syscheck/syscheck.go b/pkg/syscheck/syscheck.go deleted file mode 100644 index 2f79dd1..0000000 --- a/pkg/syscheck/syscheck.go +++ /dev/null @@ -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 -} diff --git a/pkg/tool/human/size.go b/pkg/tool/human/size.go new file mode 100644 index 0000000..af464a6 --- /dev/null +++ b/pkg/tool/human/size.go @@ -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) + } +}