chore: caddy config file(json)
nginx: proxy version api
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ dist
|
|||||||
.trae
|
.trae
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
|
forge
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"yizhisec.com/hsv2/forge/internal/cmd/installcmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func installCmd() *cobra.Command {
|
func installCmd() *cobra.Command {
|
||||||
@@ -11,15 +10,12 @@ func installCmd() *cobra.Command {
|
|||||||
Use: "install",
|
Use: "install",
|
||||||
Short: "Install the project",
|
Short: "Install the project",
|
||||||
Long: `Install the built project to the specified location.`,
|
Long: `Install the built project to the specified location.`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
return runInstall(args)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_cmd.AddCommand(
|
||||||
|
installcmd.Check(),
|
||||||
|
installcmd.Prepare(),
|
||||||
|
)
|
||||||
|
|
||||||
return _cmd
|
return _cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runInstall(args []string) error {
|
|
||||||
fmt.Println("Running install command...")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
39
internal/cmd/installcmd/check.go
Normal file
39
internal/cmd/installcmd/check.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package installcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"yizhisec.com/hsv2/forge/internal/controller/installer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Check() *cobra.Command {
|
||||||
|
var (
|
||||||
|
workdir string
|
||||||
|
target string
|
||||||
|
ignoreDisk bool
|
||||||
|
ignoreMemory bool
|
||||||
|
ignoreCPU bool
|
||||||
|
)
|
||||||
|
|
||||||
|
_cmd := &cobra.Command{
|
||||||
|
Use: "check",
|
||||||
|
Short: "Check system requirements",
|
||||||
|
Long: `Check system requirements for the project.`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
_installer := installer.NewInstaller(workdir, target)
|
||||||
|
return _installer.Check(
|
||||||
|
cmd.Context(),
|
||||||
|
installer.WithIgnoreDiskCheck(ignoreDisk),
|
||||||
|
installer.WithIgnoreMemoryCheck(ignoreMemory),
|
||||||
|
installer.WithIgnoreCPUCheck(ignoreCPU),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_cmd.Flags().StringVar(&workdir, "workdir", "/root/hs-installation", "Working directory")
|
||||||
|
_cmd.Flags().StringVar(&target, "target", "self", "Target")
|
||||||
|
_cmd.Flags().BoolVar(&ignoreDisk, "ignore-check-disk", false, "ignore disk requirement check result")
|
||||||
|
_cmd.Flags().BoolVar(&ignoreMemory, "ignore-check-memory", false, "ignore memory requirement check result")
|
||||||
|
_cmd.Flags().BoolVar(&ignoreCPU, "ignore-check-cpu", false, "ignore cpu requirement check result")
|
||||||
|
|
||||||
|
return _cmd
|
||||||
|
}
|
||||||
29
internal/cmd/installcmd/k0s.go
Normal file
29
internal/cmd/installcmd/k0s.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package installcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"yizhisec.com/hsv2/forge/internal/controller/installer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func K0s() *cobra.Command {
|
||||||
|
|
||||||
|
var (
|
||||||
|
workdir string
|
||||||
|
target string
|
||||||
|
)
|
||||||
|
|
||||||
|
_cmd := &cobra.Command{
|
||||||
|
Use: "k0s",
|
||||||
|
Short: "Install k0s",
|
||||||
|
Long: "Install k0s",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
_installer := installer.NewInstaller(workdir, target)
|
||||||
|
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
|
||||||
|
}
|
||||||
30
internal/cmd/installcmd/prepare.go
Normal file
30
internal/cmd/installcmd/prepare.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package installcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"yizhisec.com/hsv2/forge/internal/controller/installer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Prepare() *cobra.Command {
|
||||||
|
|
||||||
|
var (
|
||||||
|
workdir string
|
||||||
|
target string
|
||||||
|
)
|
||||||
|
|
||||||
|
_cmd := &cobra.Command{
|
||||||
|
Use: "prepare",
|
||||||
|
Short: "Prepare for installation",
|
||||||
|
Long: "Prepare for installation",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
_installer := installer.NewInstaller(workdir, target)
|
||||||
|
_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
|
||||||
|
}
|
||||||
@@ -1,15 +1,140 @@
|
|||||||
package installer
|
package installer
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
func (i *installer) Check(ctx context.Context) error {
|
"gitea.loveuer.com/yizhisec/pkg3/logger"
|
||||||
|
"yizhisec.com/hsv2/forge/pkg/syscheck"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CheckOption func(*checkOpt)
|
||||||
|
type checkOpt struct {
|
||||||
|
ignoreDisk bool
|
||||||
|
ignoreMemory bool
|
||||||
|
ignoreCPU bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithIgnoreDiskCheck(ignore bool) CheckOption {
|
||||||
|
return func(o *checkOpt) {
|
||||||
|
o.ignoreDisk = ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithIgnoreMemoryCheck(ignore bool) CheckOption {
|
||||||
|
return func(o *checkOpt) {
|
||||||
|
o.ignoreMemory = ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithIgnoreCPUCheck(ignore bool) CheckOption {
|
||||||
|
return func(o *checkOpt) {
|
||||||
|
o.ignoreCPU = ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *installer) Check(ctx context.Context, opts ...CheckOption) error {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
|
o = &checkOpt{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for _, fn := range opts {
|
||||||
|
fn(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("☑️ installer.Check: Starting system checks...")
|
||||||
|
|
||||||
if err = i.targetOK(ctx); err != nil {
|
if err = i.targetOK(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 !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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 !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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("✅ %s: %s", diskPerfResult.Name, diskPerfResult.Actual)
|
||||||
|
|
||||||
|
// 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 !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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 !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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("✅ %s: %s", cpuCoresResult.Name, cpuCoresResult.Actual)
|
||||||
|
|
||||||
|
// 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 !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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("✅ %s: %s", cpuFreqResult.Name, cpuFreqResult.Actual)
|
||||||
|
|
||||||
|
logger.Info("✅ installer.Check: All system checks passed successfully!")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ package installer
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"gitea.loveuer.com/yizhisec/pkg3/logger"
|
"gitea.loveuer.com/yizhisec/pkg3/logger"
|
||||||
)
|
)
|
||||||
@@ -13,14 +17,118 @@ type installer struct {
|
|||||||
target string
|
target string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *installer) targetOK(ctx context.Context) error {
|
func (i *installer) buildCommand(ctx context.Context, cmds ...string) *exec.Cmd {
|
||||||
if i.target == "" {
|
if len(cmds) == 0 {
|
||||||
logger.Debug("🎯 installer.targetOK: target = self")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// run ssh <target>, check if it's reachable, and it's root user
|
if i.target == "self" {
|
||||||
cmd := exec.CommandContext(ctx, "ssh", i.target, "whoami")
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("command failed: %w, output: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <src> <target>:<dst>
|
||||||
|
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()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug("❌ installer.targetOK: check target %s failed, err = %v", i.target, err)
|
logger.Debug("❌ installer.targetOK: check target %s failed, err = %v", i.target, err)
|
||||||
@@ -36,5 +144,10 @@ func (i *installer) targetOK(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewInstaller(workdir, target string) *installer {
|
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}
|
return &installer{workdir: workdir, target: target}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,19 @@ package installer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"gitea.loveuer.com/yizhisec/pkg3/logger"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
type K0sOpt func(*k0sOpt)
|
type K0sOpt func(*k0sOpt)
|
||||||
type k0sOpt struct {
|
type k0sOpt struct {
|
||||||
Type string // controller, worker
|
Type string // controller, worker
|
||||||
DisableWorker bool
|
controllerAsWorker bool
|
||||||
WorkerTokenFile string
|
WorkerTokenFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithK0sType(t string) K0sOpt {
|
func WithK0sType(t string) K0sOpt {
|
||||||
@@ -22,9 +26,9 @@ func WithK0sType(t string) K0sOpt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithoutK0sWorker() K0sOpt {
|
func WithK0sControllerAsWorker() K0sOpt {
|
||||||
return func(o *k0sOpt) {
|
return func(o *k0sOpt) {
|
||||||
o.DisableWorker = true
|
o.controllerAsWorker = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,9 +44,9 @@ func (i *installer) K0s(ctx context.Context, opts ...K0sOpt) error {
|
|||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
o = &k0sOpt{
|
o = &k0sOpt{
|
||||||
Type: "controller",
|
Type: "controller",
|
||||||
DisableWorker: false,
|
controllerAsWorker: false,
|
||||||
WorkerTokenFile: "/etc/k0s/worker.token",
|
WorkerTokenFile: "/etc/k0s/worker.token",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,5 +58,66 @@ func (i *installer) K0s(ctx context.Context, opts ...K0sOpt) error {
|
|||||||
fn(o)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,184 @@
|
|||||||
package installer
|
package installer
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.loveuer.com/yizhisec/pkg3/logger"
|
||||||
|
)
|
||||||
|
|
||||||
func (i *installer) Prepare(ctx context.Context) error {
|
func (i *installer) Prepare(ctx context.Context) error {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.Info("☑️ installer.Prepare: Starting system preparation...")
|
||||||
|
|
||||||
if err = i.targetOK(ctx); err != nil {
|
if err = i.targetOK(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. Set timezone to Asia/Shanghai
|
||||||
|
logger.Info("☑️ installer.Prepare: Setting timezone to Asia/Shanghai...")
|
||||||
|
if err = i.setTimezone(ctx); err != nil {
|
||||||
|
logger.Debug("❌ installer.Prepare: Failed to set timezone: %v", err)
|
||||||
|
return fmt.Errorf("failed to set timezone: %w", err)
|
||||||
|
}
|
||||||
|
logger.Info("✅ installer.Prepare: Timezone set successfully")
|
||||||
|
|
||||||
|
// 2. Disable swap
|
||||||
|
logger.Info("☑️ installer.Prepare: Disabling swap...")
|
||||||
|
if err = i.disableSwap(ctx); err != nil {
|
||||||
|
logger.Debug("❌ installer.Prepare: Failed to disable swap: %v", err)
|
||||||
|
return fmt.Errorf("failed to disable swap: %w", err)
|
||||||
|
}
|
||||||
|
logger.Info("✅ installer.Prepare: Swap disabled successfully")
|
||||||
|
|
||||||
|
// 3. Load module: iscsi_tcp
|
||||||
|
logger.Info("☑️ installer.Prepare: Loading kernel module iscsi_tcp...")
|
||||||
|
if err = i.loadKernelModule(ctx, "iscsi_tcp"); err != nil {
|
||||||
|
logger.Debug("❌ installer.Prepare: Failed to load iscsi_tcp module: %v", err)
|
||||||
|
return fmt.Errorf("failed to load iscsi_tcp module: %w", err)
|
||||||
|
}
|
||||||
|
logger.Info("✅ installer.Prepare: iscsi_tcp module loaded successfully")
|
||||||
|
|
||||||
|
// 4. Load module: br_netfilter
|
||||||
|
logger.Info("☑️ installer.Prepare: Loading kernel module br_netfilter...")
|
||||||
|
if err = i.loadKernelModule(ctx, "br_netfilter"); err != nil {
|
||||||
|
logger.Debug("❌ installer.Prepare: Failed to load br_netfilter module: %v", err)
|
||||||
|
return fmt.Errorf("failed to load br_netfilter module: %w", err)
|
||||||
|
}
|
||||||
|
logger.Info("✅ installer.Prepare: br_netfilter module loaded successfully")
|
||||||
|
|
||||||
|
// 5. Apply sysctl settings
|
||||||
|
logger.Info("☑️ installer.Prepare: Applying sysctl settings...")
|
||||||
|
if err = i.applySysctlSettings(ctx); err != nil {
|
||||||
|
logger.Debug("❌ installer.Prepare: Failed to apply sysctl settings: %v", err)
|
||||||
|
return fmt.Errorf("failed to apply sysctl settings: %w", err)
|
||||||
|
}
|
||||||
|
logger.Info("✅ installer.Prepare: Sysctl settings applied successfully")
|
||||||
|
|
||||||
|
logger.Info("✅ installer.Prepare: System preparation completed successfully!")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setTimezone sets the system timezone to Asia/Shanghai
|
||||||
|
func (i *installer) setTimezone(ctx context.Context) error {
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// disableSwap disables all swap partitions and removes swap entries from /etc/fstab
|
||||||
|
func (i *installer) disableSwap(ctx context.Context) error {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadKernelModule loads a kernel module and ensures it's loaded on boot
|
||||||
|
func (i *installer) loadKernelModule(ctx context.Context, moduleName string) error {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applySysctlSettings applies required sysctl settings for Kubernetes
|
||||||
|
func (i *installer) applySysctlSettings(ctx context.Context) error {
|
||||||
|
const sysctlConfig = `# Kubernetes required settings
|
||||||
|
net.bridge.bridge-nf-call-iptables = 1
|
||||||
|
net.bridge.bridge-nf-call-ip6tables = 1
|
||||||
|
net.ipv4.ip_forward = 1
|
||||||
|
net.ipv4.conf.all.forwarding = 1
|
||||||
|
net.ipv6.conf.all.forwarding = 1
|
||||||
|
vm.swappiness = 0
|
||||||
|
vm.overcommit_memory = 1
|
||||||
|
vm.panic_on_oom = 0
|
||||||
|
fs.file-max = 1000000
|
||||||
|
fs.inotify.max_user_watches = 2099999999
|
||||||
|
fs.inotify.max_user_instances = 2099999999
|
||||||
|
fs.inotify.max_queued_events = 2099999999
|
||||||
|
net.ipv4.neigh.default.gc_thresh1 = 1024
|
||||||
|
net.ipv4.neigh.default.gc_thresh2 = 4096
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,16 @@ kubectl create configmap ssl-client-key --namespace hsv2 --from-file=client.key=
|
|||||||
kubectl create configmap ssl-client-ca-crt --namespace hsv2 --from-file=client.ca.crt=./ssl_client_ca.crt --dry-run=client -o yaml | kubectl apply -f -
|
kubectl create configmap ssl-client-ca-crt --namespace hsv2 --from-file=client.ca.crt=./ssl_client_ca.crt --dry-run=client -o yaml | kubectl apply -f -
|
||||||
kubectl create configmap ssl-client-ca-key --namespace hsv2 --from-file=client.ca.key=./ssl_client_ca.key --dry-run=client -o yaml | kubectl apply -f -
|
kubectl create configmap ssl-client-ca-key --namespace hsv2 --from-file=client.ca.key=./ssl_client_ca.key --dry-run=client -o yaml | kubectl apply -f -
|
||||||
kubectl create configmap ssl-web-crt --namespace hsv2 --from-file=web.server.crt=./ssl_web.crt --dry-run=client -o yaml | kubectl apply -f -
|
kubectl create configmap ssl-web-crt --namespace hsv2 --from-file=web.server.crt=./ssl_web.crt --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
`
|
||||||
|
_version_yaml = `
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: config-version
|
||||||
|
namespace: hsv2
|
||||||
|
data:
|
||||||
|
version.txt: |
|
||||||
|
__version__
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -208,6 +218,12 @@ kubectl create configmap ssl-web-crt --namespace hsv2 --from-file=web.server.crt
|
|||||||
}
|
}
|
||||||
logger.Debug("✅ maker.ConfigMap: 写入 ssl_client_ca.key 文件: %s 成功", filepath.Join(dir, "ssl_client_ca.key"))
|
logger.Debug("✅ maker.ConfigMap: 写入 ssl_client_ca.key 文件: %s 成功", filepath.Join(dir, "ssl_client_ca.key"))
|
||||||
|
|
||||||
|
if err = os.WriteFile(filepath.Join(dir, "version.yaml"), []byte(_version_yaml), 0644); err != nil {
|
||||||
|
logger.Debug("❌ maker.ConfigMap: 写入 version.yaml 文件: %s 失败, 错误: %v", filepath.Join(dir, "version.yaml"), err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Debug("✅ maker.ConfigMap: 写入 version.yaml 文件: %s 成功", filepath.Join(dir, "version.yaml"))
|
||||||
|
|
||||||
// upsert configmap
|
// upsert configmap
|
||||||
logger.Debug("☑️ maker.ConfigMap: 执行 upsert 脚本: %s", filepath.Join(dir, "upsert.sh"))
|
logger.Debug("☑️ maker.ConfigMap: 执行 upsert 脚本: %s", filepath.Join(dir, "upsert.sh"))
|
||||||
if err = os.WriteFile(filepath.Join(dir, "upsert.sh"), []byte(upsert), 0755); err != nil {
|
if err = os.WriteFile(filepath.Join(dir, "upsert.sh"), []byte(upsert), 0755); err != nil {
|
||||||
|
|||||||
@@ -2,31 +2,18 @@ package maker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"gitea.loveuer.com/yizhisec/pkg3/logger"
|
"gitea.loveuer.com/yizhisec/pkg3/logger"
|
||||||
"yizhisec.com/hsv2/forge/pkg/downloader"
|
"yizhisec.com/hsv2/forge/pkg/downloader"
|
||||||
|
"yizhisec.com/hsv2/forge/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *maker) Proxy(ctx context.Context) error {
|
func (m *maker) Proxy(ctx context.Context) error {
|
||||||
const (
|
const (
|
||||||
binURL = "https://artifactory.yizhisec.com:443/artifactory/filestore/hsv2/bin/caddy"
|
binURL = "https://artifactory.yizhisec.com:443/artifactory/filestore/hsv2/bin/caddy"
|
||||||
caddyfileTpl = `{
|
|
||||||
layer4 {
|
|
||||||
:8443 {
|
|
||||||
route {
|
|
||||||
proxy __UPSTREAMS_8443__
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:443 {
|
|
||||||
route {
|
|
||||||
proxy __UPSTREAMS_443__
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
systemdSvc = `[Unit]
|
systemdSvc = `[Unit]
|
||||||
Description=YiZhiSec Caddy Reverse Proxy
|
Description=YiZhiSec Caddy Reverse Proxy
|
||||||
After=network.target
|
After=network.target
|
||||||
@@ -34,7 +21,7 @@ After=network.target
|
|||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=root
|
User=root
|
||||||
ExecStart=/usr/local/bin/caddy run --config /etc/caddy/Caddyfile
|
ExecStart=/usr/local/bin/caddy run --config /etc/caddy/caddy.json
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
StandardError=journal
|
StandardError=journal
|
||||||
Nice=-20
|
Nice=-20
|
||||||
@@ -68,12 +55,85 @@ WantedBy=multi-user.target`
|
|||||||
}
|
}
|
||||||
logger.Debug("✅ maker.Proxy: 下载 caddy 成功, url = %s", binURL)
|
logger.Debug("✅ maker.Proxy: 下载 caddy 成功, url = %s", binURL)
|
||||||
|
|
||||||
logger.Debug("☑️ maker.Proxy: 写入 Caddyfile 文件..., dest = %s", filepath.Join(location, "Caddyfile"))
|
logger.Debug("☑️ maker.Proxy: 写入 caddy.json 文件..., dest = %s", filepath.Join(location, "caddy.json"))
|
||||||
if err := os.WriteFile(filepath.Join(location, "Caddyfile"), []byte(caddyfileTpl), 0644); err != nil {
|
caddyConfig := model.CaddyConfig{
|
||||||
logger.Debug("❌ maker.Proxy: 写入 Caddyfile 失败, dest = %s, err = %v", filepath.Join(location, "Caddyfile"), err)
|
"apps": &model.CaddyApp{
|
||||||
|
Layer4: &model.CaddyLayer4{
|
||||||
|
Servers: map[string]*model.CaddyServer{
|
||||||
|
"proxy_8443": {
|
||||||
|
Listen: []string{":8443"},
|
||||||
|
Routes: []*model.CaddyRoute{
|
||||||
|
{
|
||||||
|
Handle: []*model.CaddyHandle{
|
||||||
|
{
|
||||||
|
Handler: "proxy",
|
||||||
|
Upstreams: []*model.CaddyUpstream{
|
||||||
|
{Dial: []string{"__ip_1__:32443"}},
|
||||||
|
{Dial: []string{"__ip_2__:32443"}},
|
||||||
|
},
|
||||||
|
HealthChecks: &model.CaddyHealthCheck{
|
||||||
|
Active: &model.CaddyActive{
|
||||||
|
Interval: "10s",
|
||||||
|
Timeout: "2s",
|
||||||
|
Port: 32443,
|
||||||
|
},
|
||||||
|
Passive: &model.CaddyPassive{
|
||||||
|
FailDuration: "30s",
|
||||||
|
MaxFails: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LoadBalancing: &model.CaddyLoadBalancing{
|
||||||
|
Selection: &model.CaddySelection{
|
||||||
|
Policy: "round_robin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"proxy_443": {
|
||||||
|
Listen: []string{":443"},
|
||||||
|
Routes: []*model.CaddyRoute{
|
||||||
|
{
|
||||||
|
Handle: []*model.CaddyHandle{
|
||||||
|
{
|
||||||
|
Handler: "proxy",
|
||||||
|
Upstreams: []*model.CaddyUpstream{
|
||||||
|
{Dial: []string{"__ip_1__:31443"}},
|
||||||
|
{Dial: []string{"__ip_2__:31443"}},
|
||||||
|
},
|
||||||
|
HealthChecks: &model.CaddyHealthCheck{
|
||||||
|
Active: &model.CaddyActive{
|
||||||
|
Interval: "10s",
|
||||||
|
Timeout: "2s",
|
||||||
|
Port: 31443,
|
||||||
|
},
|
||||||
|
Passive: &model.CaddyPassive{
|
||||||
|
FailDuration: "30s",
|
||||||
|
MaxFails: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LoadBalancing: &model.CaddyLoadBalancing{
|
||||||
|
Selection: &model.CaddySelection{
|
||||||
|
Policy: "round_robin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
bs, _ := json.MarshalIndent(caddyConfig, "", " ")
|
||||||
|
if err := os.WriteFile(filepath.Join(location, "caddy.json"), []byte(bs), 0644); err != nil {
|
||||||
|
logger.Debug("❌ maker.Proxy: 写入 Caddyfile 失败, dest = %s, err = %v", filepath.Join(location, "caddy.json"), err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logger.Debug("✅ maker.Proxy: 写入 Caddyfile 文件成功, dest = %s", filepath.Join(location, "Caddyfile"))
|
logger.Debug("✅ maker.Proxy: 写入 Caddyfile 文件成功, dest = %s", filepath.Join(location, "caddy.json"))
|
||||||
|
|
||||||
logger.Debug("☑️ maker.Proxy: 写入 caddy.service 文件..., dest = %s", filepath.Join(location, "caddy.service"))
|
logger.Debug("☑️ maker.Proxy: 写入 caddy.service 文件..., dest = %s", filepath.Join(location, "caddy.service"))
|
||||||
if err := os.WriteFile(filepath.Join(location, "caddy.service"), []byte(systemdSvc), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(location, "caddy.service"), []byte(systemdSvc), 0644); err != nil {
|
||||||
|
|||||||
55
pkg/model/caddy.go
Normal file
55
pkg/model/caddy.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type CaddyUpstream struct {
|
||||||
|
Dial []string `json:"dial"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaddyActive struct {
|
||||||
|
Interval string `json:"interval"`
|
||||||
|
Timeout string `json:"timeout"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaddyPassive struct {
|
||||||
|
MaxFails int `json:"max_fails"`
|
||||||
|
FailDuration string `json:"fail_duration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaddyHealthCheck struct {
|
||||||
|
Active *CaddyActive `json:"active"`
|
||||||
|
Passive *CaddyPassive `json:"passive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaddySelection struct {
|
||||||
|
Policy string `json:"policy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaddyLoadBalancing struct {
|
||||||
|
Selection *CaddySelection `json:"selection"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaddyHandle struct {
|
||||||
|
Handler string `json:"handler"`
|
||||||
|
Upstreams []*CaddyUpstream `json:"upstreams"`
|
||||||
|
HealthChecks *CaddyHealthCheck `json:"health_checks"`
|
||||||
|
LoadBalancing *CaddyLoadBalancing `json:"load_balancing"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaddyRoute struct {
|
||||||
|
Handle []*CaddyHandle `json:"handle"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaddyServer struct {
|
||||||
|
Listen []string `json:"listen"`
|
||||||
|
Routes []*CaddyRoute `json:"routes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaddyLayer4 struct {
|
||||||
|
Servers map[string]*CaddyServer `json:"servers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaddyApp struct {
|
||||||
|
Layer4 *CaddyLayer4 `json:"layer4"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaddyConfig map[string]*CaddyApp
|
||||||
73
pkg/resource/nginx/caddy.json
Normal file
73
pkg/resource/nginx/caddy.json
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"layer4": {
|
||||||
|
"servers": {
|
||||||
|
"proxy_8443_tcp_backends": {
|
||||||
|
"listen": [":8443"],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "proxy",
|
||||||
|
"upstreams": [
|
||||||
|
{"dial": ["10.118.2.11:32443"]},
|
||||||
|
{"dial": ["10.118.2.12:32443"]}
|
||||||
|
],
|
||||||
|
"health_checks": {
|
||||||
|
"active": {
|
||||||
|
"interval": "5s",
|
||||||
|
"timeout": "2s",
|
||||||
|
"port": 32443
|
||||||
|
},
|
||||||
|
"passive": {
|
||||||
|
"max_fails": 1,
|
||||||
|
"fail_duration": "30s"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"load_balancing": {
|
||||||
|
"selection": {
|
||||||
|
"policy": "round_robin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"proxy_443_tcp_backends": {
|
||||||
|
"listen": [":443"],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "proxy",
|
||||||
|
"upstreams": [
|
||||||
|
{"dial": ["10.118.2.11:31443"]},
|
||||||
|
{"dial": ["10.118.2.12:31443"]}
|
||||||
|
],
|
||||||
|
"health_checks": {
|
||||||
|
"active": {
|
||||||
|
"interval": "5s",
|
||||||
|
"timeout": "2s",
|
||||||
|
"port": 31443
|
||||||
|
},
|
||||||
|
"passive": {
|
||||||
|
"max_fails": 1,
|
||||||
|
"fail_duration": "30s"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"load_balancing": {
|
||||||
|
"selection": {
|
||||||
|
"policy": "round_robin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -37,6 +37,10 @@ server {
|
|||||||
proxy_pass http://u-api-service/api/v2_2/client/download/check;
|
proxy_pass http://u-api-service/api/v2_2/client/download/check;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /api/v1/version {
|
||||||
|
proxy_pass http://u-api-service/api/v2_2/client/version;
|
||||||
|
}
|
||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://hs-client-server;
|
proxy_pass http://hs-client-server;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
@@ -140,10 +144,6 @@ server {
|
|||||||
|
|
||||||
client_max_body_size 50M;
|
client_max_body_size 50M;
|
||||||
|
|
||||||
# location /api/v1/pkg/config/setup {
|
|
||||||
# proxy_pass http://u-api-service/api/v2_2/client/download/version;
|
|
||||||
# }
|
|
||||||
|
|
||||||
location /api/v1/pkg/archive {
|
location /api/v1/pkg/archive {
|
||||||
proxy_pass http://u-api-service/api/v2_2/client/download/check;
|
proxy_pass http://u-api-service/api/v2_2/client/download/check;
|
||||||
}
|
}
|
||||||
@@ -152,18 +152,22 @@ server {
|
|||||||
proxy_pass http://u-api-service/api/v2_2/client/download/version;
|
proxy_pass http://u-api-service/api/v2_2/client/download/version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /api/v1/version {
|
||||||
|
proxy_pass http://u-api-service/api/v2_2/client/version;
|
||||||
|
}
|
||||||
|
|
||||||
location /static/config/rc.json {
|
location /static/config/rc.json {
|
||||||
proxy_pass http://u-api-service/api/v2_2/client/rc/json?os=win;
|
proxy_pass http://u-api-service/api/v2_2/client/rc/json?os=win;
|
||||||
}
|
}
|
||||||
|
|
||||||
location = /api/v1/version {
|
# location = /api/v1/version {
|
||||||
proxy_pass http://hs-client-without-auth-server;
|
# proxy_pass http://hs-client-without-auth-server;
|
||||||
proxy_http_version 1.1;
|
# proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
# proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $proxy_protocol_addr;
|
# proxy_set_header X-Real-IP $proxy_protocol_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_read_timeout 300s;
|
# proxy_read_timeout 300s;
|
||||||
}
|
# }
|
||||||
|
|
||||||
location /api/v1/pkg {
|
location /api/v1/pkg {
|
||||||
proxy_pass http://hs-client-without-auth-server;
|
proxy_pass http://hs-client-without-auth-server;
|
||||||
|
|||||||
318
pkg/syscheck/syscheck.go
Normal file
318
pkg/syscheck/syscheck.go
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user