diff --git a/neonvm-controller/cmd/main.go b/neonvm-controller/cmd/main.go index c6acb6cf6..878c720b8 100644 --- a/neonvm-controller/cmd/main.go +++ b/neonvm-controller/cmd/main.go @@ -42,7 +42,6 @@ import ( "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" diff --git a/pkg/neonvm/cpuscaling/sysfsscaling.go b/pkg/neonvm/cpuscaling/sysfsscaling.go new file mode 100644 index 000000000..b8da5992d --- /dev/null +++ b/pkg/neonvm/cpuscaling/sysfsscaling.go @@ -0,0 +1,196 @@ +package cpuscaling + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" +) + +// CPU directory path +const cpuPath = "/sys/devices/system/cpu/" + +type CPUSysFsStateScaler struct { +} + +func (c *CPUSysFsStateScaler) EnsureOnlineCPUs(X int) error { + cpus, err := getAllCPUs() + if err != nil { + return err + } + + onlineCount, err := c.GetActiveCPUsCount() + if err != nil { + return err + } + + if onlineCount < uint32(X) { + for _, cpu := range cpus { + if cpu == 0 { + // Skip CPU 0 as it is always online and can't be toggled + continue + } + + online, err := isCPUOnline(cpu) + if err != nil { + return err + } + + if !online { + // Mark CPU as online + err := setCPUOnline(cpu, true) + if err != nil { + return err + } + onlineCount++ + } + + // Stop when we reach the target count + if onlineCount == uint32(X) { + break + } + } + } else if onlineCount > uint32(X) { + // Remove CPUs if there are more than X online + for i := len(cpus) - 1; i >= 0; i-- { + cpu := cpus[i] + if cpu == 0 { + // Skip CPU 0 as it cannot be taken offline + continue + } + + online, err := isCPUOnline(cpu) + if err != nil { + return err + } + + if online { + // Mark CPU as offline + err := setCPUOnline(cpu, false) + if err != nil { + return err + } + onlineCount-- + } + + // Stop when we reach the target count + if onlineCount == uint32(X) { + break + } + } + } + + if onlineCount != uint32(X) { + return fmt.Errorf("failed to ensure %d CPUs are online, current online CPUs: %d", X, onlineCount) + } + + return nil +} + +// GetActiveCPUsCount() returns the count of online CPUs. +func (c *CPUSysFsStateScaler) GetActiveCPUsCount() (uint32, error) { + cpus, err := getAllCPUs() + if err != nil { + return 0, err + } + + var onlineCount uint32 + for _, cpu := range cpus { + online, err := isCPUOnline(cpu) + if err != nil { + return 0, err + } + if online { + onlineCount++ + } + } + + return onlineCount, nil +} + +// GetTotalCPUsCount returns the total number of CPUs (online + offline). +func (c *CPUSysFsStateScaler) GetTotalCPUsCount() (uint32, error) { + cpus, err := getAllCPUs() + if err != nil { + return 0, err + } + + return uint32(len(cpus)), nil +} + +// Helper functions + +// getAllCPUs returns a list of all CPUs that are physically present in the system. +func getAllCPUs() ([]int, error) { + data, err := os.ReadFile(filepath.Join(cpuPath, "possible")) + if err != nil { + return nil, err + } + + return parseCPURange(string(data)) +} + +// parseCPURange parses the CPU range string (e.g., "0-3") and returns a list of CPUs. +func parseCPURange(cpuRange string) ([]int, error) { + var cpus []int + cpuRange = strings.TrimSpace(cpuRange) + parts := strings.Split(cpuRange, "-") + + if len(parts) == 1 { + // Single CPU case, e.g., "0" + cpu, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, err + } + cpus = append(cpus, cpu) + } else if len(parts) == 2 { + // Range case, e.g., "0-3" + start, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, err + } + end, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, err + } + for i := start; i <= end; i++ { + cpus = append(cpus, i) + } + } + + return cpus, nil +} + +// isCPUOnline checks if a given CPU is online. +func isCPUOnline(cpu int) (bool, error) { + data, err := os.ReadFile(filepath.Join(cpuPath, fmt.Sprintf("cpu%d/online", cpu))) + if os.IsNotExist(err) { + // If the file doesn't exist for CPU 0, assume it's online + if cpu == 0 { + return true, nil + } + return false, err + } + if err != nil { + return false, err + } + + online := strings.TrimSpace(string(data)) + return online == "1", nil +} + +// setCPUOnline sets the given CPU as online (true) or offline (false). +func setCPUOnline(cpu int, online bool) error { + state := "0" + if online { + state = "1" + } + + err := os.WriteFile(filepath.Join(cpuPath, fmt.Sprintf("cpu%d/online", cpu)), []byte(state), 0644) + if err != nil { + return fmt.Errorf("failed to set CPU %d online status: %w", cpu, err) + } + + return nil +} diff --git a/vm-builder/files/Dockerfile.img b/vm-builder/files/Dockerfile.img index 990ae8617..0abed8262 100644 --- a/vm-builder/files/Dockerfile.img +++ b/vm-builder/files/Dockerfile.img @@ -21,7 +21,7 @@ RUN set -e \ chmod +x /neonvm/bin/busybox \ && /neonvm/bin/busybox --install -s /neonvm/bin - COPY helper.move-bins.sh /helper.move-bins.sh +COPY helper.move-bins.sh /helper.move-bins.sh # add udevd and agetty (with shared libs) RUN set -e \ diff --git a/vm-builder/files/agetty-init-amd64 b/vm-builder/files/agetty-init-amd64 new file mode 100644 index 000000000..894a24ae4 --- /dev/null +++ b/vm-builder/files/agetty-init-amd64 @@ -0,0 +1 @@ +ttyS0::respawn:/neonvm/bin/agetty --8bits --local-line --noissue --noclear --noreset --host console --login-program /neonvm/bin/login --login-pause --autologin root 115200 ttyS0 linux diff --git a/vm-builder/files/agetty-init-arm64 b/vm-builder/files/agetty-init-arm64 new file mode 100644 index 000000000..713a4f669 --- /dev/null +++ b/vm-builder/files/agetty-init-arm64 @@ -0,0 +1 @@ +ttyAMA0::respawn:/neonvm/bin/agetty --8bits --local-line --noissue --noclear --noreset --host console --login-program /neonvm/bin/login --login-pause --autologin root 115200 ttyAMA0 linux diff --git a/vm-builder/files/inittab b/vm-builder/files/inittab index 9f7c284ec..e9ef19f1c 100644 --- a/vm-builder/files/inittab +++ b/vm-builder/files/inittab @@ -12,5 +12,5 @@ {{ range .InittabCommands }} ::{{.SysvInitAction}}:su -p {{.CommandUser}} -c {{.ShellEscapedCommand}} {{ end }} -ttyAMA0::respawn:/neonvm/bin/agetty --8bits --local-line --noissue --noclear --noreset --host console --login-program /neonvm/bin/login --login-pause --autologin root 115200 ttyAMA0 linux +{{ .AgettyInitLine }} ::shutdown:/neonvm/bin/vmshutdown diff --git a/vm-builder/main.go b/vm-builder/main.go index 6fc354643..ef3b1f14a 100644 --- a/vm-builder/main.go +++ b/vm-builder/main.go @@ -38,6 +38,10 @@ var ( scriptVmStart string //go:embed files/inittab scriptInitTab string + //go:embed files/agetty-init-amd64 + scriptAgettyInitAmd64 string + //go:embed files/agetty-init-arm64 + scriptAgettyInitArm64 string //go:embed files/vmacpi scriptVmAcpi string //go:embed files/vmshutdown @@ -58,6 +62,11 @@ var ( configSshd string ) +const ( + targetArchLinuxAmd64 = "linux/amd64" + targetArchLinuxArm64 = "linux/arm64" +) + var ( Version string NeonvmDaemonImage string @@ -72,7 +81,7 @@ var ( version = flag.Bool("version", false, `Print vm-builder version`) daemonImageFlag = flag.String("daemon-image", "", `Specify the neonvm-daemon image: --daemon-image=neonvm-daemon:dev`) - targetArch = flag.String("target-arch", "linux/amd64", `Target architecture: --arch linux/amd64`) + targetArch = flag.String("target-arch", "", fmt.Sprintf("Target architecture: --arch %s | %s", targetArchLinuxAmd64, targetArchLinuxArm64)) ) func AddTemplatedFileToTar(tw *tar.Writer, tmplArgs any, filename string, tmplString string) error { @@ -117,6 +126,7 @@ type TemplatesContext struct { SpecBuild string SpecMerge string InittabCommands []inittabCommand + AgettyInitLine string ShutdownHook string } @@ -133,13 +143,24 @@ func main() { fmt.Println(Version) os.Exit(0) } - if len(*daemonImageFlag) == 0 && len(NeonvmDaemonImage) == 0 { log.Println("neonvm-daemon image not set, needs to be explicitly passed in, or compiled with -ldflags '-X main.NeonvmDaemonImage=...'") flag.PrintDefaults() os.Exit(1) } + if targetArch == nil || *targetArch == "" { + log.Println("Target architecture not set, see usage info:") + flag.PrintDefaults() + os.Exit(1) + } + + if *targetArch != targetArchLinuxAmd64 && *targetArch != targetArchLinuxArm64 { + log.Fatalf("Unsupported target architecture: %q", *targetArch) + flag.PrintDefaults() + return + } + neonvmDaemonImage := NeonvmDaemonImage if len(*daemonImageFlag) != 0 { neonvmDaemonImage = *daemonImageFlag @@ -290,7 +311,8 @@ func main() { SpecBuild: "", // overridden below if spec != nil SpecMerge: "", // overridden below if spec != nil InittabCommands: nil, // overridden below if spec != nil - ShutdownHook: "", // overridden below if spec != nil + AgettyInitLine: getAgettyInitLine(*targetArch), + ShutdownHook: "", // overridden below if spec != nil } if len(imageSpec.Config.User) != 0 { @@ -347,6 +369,8 @@ func main() { {"vmstart", scriptVmStart}, {"vmshutdown", scriptVmShutdown}, {"inittab", scriptInitTab}, + {"agetty-init-amd64", scriptAgettyInitAmd64}, + {"agetty-init-arm64", scriptAgettyInitArm64}, {"vmacpi", scriptVmAcpi}, {"vminit", scriptVmInit}, {"vector.yaml", configVector}, @@ -543,3 +567,15 @@ func (f file) validate() []error { return errs } + +func getAgettyInitLine(targetArch string) string { + switch targetArch { + case targetArchLinuxAmd64: + return scriptAgettyInitAmd64 + case targetArchLinuxArm64: + return scriptAgettyInitArm64 + default: + log.Fatalf("Unsupported target architecture: %q", targetArch) + return "" + } +}