From 1ea5535f2e2ef1345bb4ec90819d0a83e3ba9c8c Mon Sep 17 00:00:00 2001 From: markmental Date: Mon, 15 Dec 2025 13:16:09 -0500 Subject: [PATCH] Initial Commit --- go.mod | 3 + main.go | 256 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 go.mod create mode 100644 main.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..01f39d8 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module tux-microvm + +go 1.24.4 diff --git a/main.go b/main.go new file mode 100644 index 0000000..23012f2 --- /dev/null +++ b/main.go @@ -0,0 +1,256 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" +) + +const ( + rootDir = "/var/lib/microvm" + imageDir = rootDir + "/images" + vmDirBase = rootDir + "/vms" + + defaultImgURL = "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img" + defaultImgName = "ubuntu-24.04-cloudinit.qcow2" + + memMB = "1024" + cpus = "1" +) + +func ensureDirs() { + for _, d := range []string{imageDir, vmDirBase} { + if err := os.MkdirAll(d, 0755); err != nil { + panic(err) + } + } +} + +func baseImagePath() string { + return filepath.Join(imageDir, defaultImgName) +} + +func vmDir(name string) string { return filepath.Join(vmDirBase, name) } +func pidFile(name string) string { return filepath.Join(vmDir(name), "vm.pid") } +func diskPath(name string) string { + return filepath.Join(vmDir(name), "disk.qcow2") +} +func seedISOPath(name string) string { + return filepath.Join(vmDir(name), "seed.iso") +} + +func sshPort(name string) int { + if strings.HasPrefix(name, "vm") { + if n, err := strconv.Atoi(strings.TrimPrefix(name, "vm")); err == nil { + return 2220 + n + } + } + return 2222 +} + +func readSSHPublicKey() string { + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + + keys := []string{ + filepath.Join(home, ".ssh", "id_ed25519.pub"), + filepath.Join(home, ".ssh", "id_rsa.pub"), + } + + for _, k := range keys { + if data, err := os.ReadFile(k); err == nil { + return strings.TrimSpace(string(data)) + } + } + + panic("no SSH public key found in ~/.ssh") +} + +func createCloudInitSeed(name string) { + sshKey := readSSHPublicKey() + + userData := fmt.Sprintf(`#cloud-config +users: + - name: ubuntu + sudo: ALL=(ALL) NOPASSWD:ALL + shell: /bin/bash + ssh_authorized_keys: + - %s + +ssh_pwauth: false +disable_root: true +`, sshKey) + + metaData := fmt.Sprintf(`instance-id: %s +local-hostname: %s +`, name, name) + + tmpDir, err := os.MkdirTemp("", "cloudinit") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpDir) + + userDataPath := filepath.Join(tmpDir, "user-data") + metaDataPath := filepath.Join(tmpDir, "meta-data") + + os.WriteFile(userDataPath, []byte(userData), 0644) + os.WriteFile(metaDataPath, []byte(metaData), 0644) + + cmd := exec.Command( + "genisoimage", + "-output", seedISOPath(name), + "-volid", "cidata", + "-joliet", + "-rock", + userDataPath, + metaDataPath, + ) + + if out, err := cmd.CombinedOutput(); err != nil { + panic("cloud-init ISO creation failed:\n" + string(out)) + } +} + +func ensureBaseImage() { + if _, err := os.Stat(baseImagePath()); err == nil { + return + } + + fmt.Println("Downloading Ubuntu 24.04 cloud image...") + + resp, err := http.Get(defaultImgURL) + if err != nil { + panic(err) + } + defer resp.Body.Close() + + out, err := os.Create(baseImagePath()) + if err != nil { + panic(err) + } + defer out.Close() + + if _, err := io.Copy(out, resp.Body); err != nil { + panic(err) + } +} + +func createVM(name string) { + ensureBaseImage() + os.MkdirAll(vmDir(name), 0755) + + cmd := exec.Command( + "qemu-img", "create", + "-f", "qcow2", + "-F", "qcow2", + "-b", baseImagePath(), + diskPath(name), + ) + + if out, err := cmd.CombinedOutput(); err != nil { + panic(string(out)) + } + + createCloudInitSeed(name) + + fmt.Println("VM created:", name) +} + +func startVM(name string) { + if data, err := os.ReadFile(pidFile(name)); err == nil { + pid := strings.TrimSpace(string(data)) + if _, err := os.Stat("/proc/" + pid); err == nil { + panic("VM already running") + } + os.Remove(pidFile(name)) + } + + port := sshPort(name) + + cmd := exec.Command( + "qemu-system-x86_64", + "-enable-kvm", + "-machine", "q35", + "-cpu", "host", + "-m", memMB, + "-smp", cpus, + + // VNC-only console + "-vga", "virtio", + "-display", "vnc=0.0.0.0:1", + + // Main disk (cloud image) + "-drive", fmt.Sprintf("file=%s,if=virtio,format=qcow2", diskPath(name)), + + // Cloud-init seed ISO + "-drive", fmt.Sprintf("file=%s,if=virtio,format=raw", seedISOPath(name)), + + // User-mode networking + SSH forward + "-netdev", fmt.Sprintf("user,id=net0,hostfwd=tcp::%d-:22", port), + "-device", "virtio-net-pci,netdev=net0", + + // Boot from disk (GRUB inside image) + "-boot", "order=c", + + "-pidfile", pidFile(name), + "-daemonize", + ) + + + if out, err := cmd.CombinedOutput(); err != nil { + panic("qemu failed:\n" + string(out)) + } + + fmt.Println("VM started:", name) + fmt.Printf("VNC: localhost:5901\n") + fmt.Printf("SSH: ssh ubuntu@localhost -p %d\n", port) +} + +func stopVM(name string) { + data, err := os.ReadFile(pidFile(name)) + if err != nil { + panic("VM not running") + } + pid := strings.TrimSpace(string(data)) + exec.Command("kill", pid).Run() + os.Remove(pidFile(name)) + fmt.Println("VM stopped:", name) +} + +func listVMs() { + entries, _ := os.ReadDir(vmDirBase) + for _, e := range entries { + fmt.Println(e.Name()) + } +} + +func main() { + if len(os.Args) < 2 { + fmt.Println("usage: microvm ") + os.Exit(1) + } + + ensureDirs() + + switch os.Args[1] { + case "create": + createVM(os.Args[2]) + case "start": + startVM(os.Args[2]) + case "stop": + stopVM(os.Args[2]) + case "list": + listVMs() + default: + fmt.Println("usage: microvm ") + } +} +