renamed to mnmivm, more detailed list view

This commit is contained in:
markmental 2025-12-15 15:10:22 -05:00
commit 010d78a1cc
3 changed files with 260 additions and 253 deletions

View file

@ -1,2 +1,2 @@
#!/bin/sh #!/bin/sh
go build -o ./microvm go build -o ./mnmivm

203
main.go
View file

@ -3,8 +3,8 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"net/http"
"net" "net"
"net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -64,11 +64,11 @@ func ensureDirs() {
func usage() { func usage() {
fmt.Println("usage:") fmt.Println("usage:")
fmt.Println(" microvm create <name> --os <ubuntu|debian|fedora|alpine> --pubkey-path <path>") fmt.Println(" mnmivm create <name> --os <ubuntu|debian|fedora|alpine> --pubkey-path <path>")
fmt.Println(" microvm start <name>") fmt.Println(" mnmivm start <name>")
fmt.Println(" microvm stop <name>") fmt.Println(" mnmivm stop <name>")
fmt.Println(" microvm delete <name>") fmt.Println(" mnmivm delete <name>")
fmt.Println(" microvm list") fmt.Println(" mnmivm list")
os.Exit(1) os.Exit(1)
} }
@ -82,55 +82,6 @@ func imagePath(osName string) string {
return filepath.Join(imageDir, osImages[osName].Filename) return filepath.Join(imageDir, osImages[osName].Filename)
} }
func deleteVM(name string) {
dir := vmDir(name)
if _, err := os.Stat(dir); err != nil {
panic("VM does not exist: " + name)
}
// If running, stop it first
if data, err := os.ReadFile(pidFile(name)); err == nil {
pid := strings.TrimSpace(string(data))
if _, err := os.Stat("/proc/" + pid); err == nil {
fmt.Println("VM is running, stopping it first...")
exec.Command("kill", pid).Run()
}
os.Remove(pidFile(name))
}
fmt.Printf(`
WARNING: This will permanently delete the VM "%s"
Path: %s
Type YES (all caps) to confirm: `, name, dir)
var confirm string
fmt.Scanln(&confirm)
if confirm != "YES" {
fmt.Println("Aborted. VM not deleted.")
return
}
if err := os.RemoveAll(dir); err != nil {
panic(err)
}
fmt.Println("VM deleted:", name)
}
func findFreePort() int {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(err)
}
defer l.Close()
return l.Addr().(*net.TCPAddr).Port
}
func parseArg(args []string, key string) string { func parseArg(args []string, key string) string {
for i := 0; i < len(args)-1; i++ { for i := 0; i < len(args)-1; i++ {
if args[i] == key { if args[i] == key {
@ -140,16 +91,13 @@ func parseArg(args []string, key string) string {
panic("missing required argument: " + key) panic("missing required argument: " + key)
} }
func readPublicKey(path string) string { func findFreePort() int {
data, err := os.ReadFile(path) l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
panic(err) panic(err)
} }
key := strings.TrimSpace(string(data)) defer l.Close()
if !strings.HasPrefix(key, "ssh-") { return l.Addr().(*net.TCPAddr).Port
panic("invalid SSH public key")
}
return key
} }
func ensureBaseImage(osName string) { func ensureBaseImage(osName string) {
@ -161,7 +109,6 @@ func ensureBaseImage(osName string) {
} }
fmt.Println("Downloading", img.Name, "cloud image...") fmt.Println("Downloading", img.Name, "cloud image...")
resp, err := http.Get(img.URL) resp, err := http.Get(img.URL)
if err != nil { if err != nil {
panic(err) panic(err)
@ -179,6 +126,18 @@ func ensureBaseImage(osName string) {
} }
} }
func readPublicKey(path string) string {
data, err := os.ReadFile(path)
if err != nil {
panic(err)
}
key := strings.TrimSpace(string(data))
if !strings.HasPrefix(key, "ssh-") {
panic("invalid SSH public key")
}
return key
}
func createCloudInitSeed(name, osName, pubKeyPath string) { func createCloudInitSeed(name, osName, pubKeyPath string) {
img := osImages[osName] img := osImages[osName]
sshKey := readPublicKey(pubKeyPath) sshKey := readPublicKey(pubKeyPath)
@ -190,14 +149,11 @@ users:
shell: /bin/sh shell: /bin/sh
ssh_authorized_keys: ssh_authorized_keys:
- %s - %s
ssh_pwauth: false ssh_pwauth: false
disable_root: true disable_root: true
`, img.User, sshKey) `, img.User, sshKey)
metaData := fmt.Sprintf(`instance-id: %s metaData := fmt.Sprintf("instance-id: %s\nlocal-hostname: %s\n", name, name)
local-hostname: %s
`, name, name)
tmpDir, _ := os.MkdirTemp("", "cloudinit") tmpDir, _ := os.MkdirTemp("", "cloudinit")
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
@ -228,9 +184,9 @@ func createVM(name, osName, pubKeyPath string) {
ensureBaseImage(osName) ensureBaseImage(osName)
os.MkdirAll(vmDir(name), 0755) os.MkdirAll(vmDir(name), 0755)
keyDst := vmPubKeyPath(name) keyData, _ := os.ReadFile(pubKeyPath)
data, _ := os.ReadFile(pubKeyPath) os.WriteFile(vmPubKeyPath(name), keyData, 0644)
os.WriteFile(keyDst, data, 0644) os.WriteFile(filepath.Join(vmDir(name), "os.name"), []byte(osName), 0644)
cmd := exec.Command( cmd := exec.Command(
"qemu-img", "create", "qemu-img", "create",
@ -244,8 +200,7 @@ func createVM(name, osName, pubKeyPath string) {
panic(string(out)) panic(string(out))
} }
createCloudInitSeed(name, osName, keyDst) createCloudInitSeed(name, osName, vmPubKeyPath(name))
fmt.Println("VM created:", name, "OS:", osName) fmt.Println("VM created:", name, "OS:", osName)
} }
@ -257,14 +212,13 @@ func startVM(name string) {
os.Remove(pidFile(name)) os.Remove(pidFile(name))
} }
port := findFreePort() sshPort := findFreePort()
vncPort := findFreePort() vncPort := findFreePort()
vncDisplay := vncPort - 5900 vncDisplay := vncPort - 5900
if vncDisplay < 0 { if vncDisplay < 0 {
panic("invalid VNC port allocation") panic("invalid VNC port")
} }
cmd := exec.Command( cmd := exec.Command(
"qemu-system-x86_64", "qemu-system-x86_64",
"-enable-kvm", "-enable-kvm",
@ -272,17 +226,12 @@ func startVM(name string) {
"-cpu", "host", "-cpu", "host",
"-m", memMB, "-m", memMB,
"-smp", cpus, "-smp", cpus,
"-vga", "virtio", "-vga", "virtio",
"-display", fmt.Sprintf("vnc=0.0.0.0:%d", vncDisplay), "-display", fmt.Sprintf("vnc=0.0.0.0:%d", vncDisplay),
"-drive", fmt.Sprintf("file=%s,if=virtio,format=qcow2", diskPath(name)), "-drive", fmt.Sprintf("file=%s,if=virtio,format=qcow2", diskPath(name)),
"-drive", fmt.Sprintf("file=%s,if=virtio,format=raw", seedISOPath(name)), "-drive", fmt.Sprintf("file=%s,if=virtio,format=raw", seedISOPath(name)),
"-netdev", fmt.Sprintf("user,id=net0,hostfwd=tcp::%d-:22", sshPort),
"-netdev", fmt.Sprintf("user,id=net0,hostfwd=tcp::%d-:22", port),
"-device", "virtio-net-pci,netdev=net0", "-device", "virtio-net-pci,netdev=net0",
"-boot", "order=c",
"-pidfile", pidFile(name), "-pidfile", pidFile(name),
"-daemonize", "-daemonize",
) )
@ -291,9 +240,12 @@ func startVM(name string) {
panic(string(out)) panic(string(out))
} }
os.WriteFile(filepath.Join(vmDir(name), "ssh.port"), []byte(fmt.Sprint(sshPort)), 0644)
os.WriteFile(filepath.Join(vmDir(name), "vnc.port"), []byte(fmt.Sprint(vncPort)), 0644)
fmt.Println("VM started:", name) fmt.Println("VM started:", name)
fmt.Printf("VNC: vnc://127.0.0.1:%d\n", vncPort) fmt.Printf("SSH: ssh %s@localhost -p %d\n", osImages[readFileTrim(filepath.Join(vmDir(name), "os.name"))].User, sshPort)
fmt.Printf("SSH: ssh <user>@localhost -p %d\n", port) fmt.Printf("VNC: vnc://0.0.0.0:%d\n", vncPort)
} }
func stopVM(name string) { func stopVM(name string) {
@ -303,10 +255,78 @@ func stopVM(name string) {
fmt.Println("VM stopped:", name) fmt.Println("VM stopped:", name)
} }
func deleteVM(name string) {
dir := vmDir(name)
if _, err := os.Stat(dir); err != nil {
panic("VM does not exist")
}
if data, err := os.ReadFile(pidFile(name)); err == nil {
exec.Command("kill", strings.TrimSpace(string(data))).Run()
os.Remove(pidFile(name))
}
fmt.Printf("\nWARNING: Permanently delete VM \"%s\"?\nType YES to confirm: ", name)
var confirm string
fmt.Scanln(&confirm)
if confirm != "YES" {
fmt.Println("Aborted.")
return
}
os.RemoveAll(dir)
fmt.Println("VM deleted:", name)
}
func readFileTrim(path string) string {
data, err := os.ReadFile(path)
if err != nil {
return "-"
}
return strings.TrimSpace(string(data))
}
func vmRunning(name string) bool {
data, err := os.ReadFile(pidFile(name))
if err != nil {
return false
}
_, err = os.Stat("/proc/" + strings.TrimSpace(string(data)))
return err == nil
}
func listVMs() { func listVMs() {
entries, _ := os.ReadDir(vmDirBase) entries, _ := os.ReadDir(vmDirBase)
fmt.Printf("%-12s %-8s %-8s %-16s %-16s %s\n",
"NAME", "STATE", "OS", "SSH", "VNC", "PUBKEY")
fmt.Println(strings.Repeat("-", 90))
for _, e := range entries { for _, e := range entries {
fmt.Println(e.Name()) name := e.Name()
dir := vmDir(name)
state := "stopped"
if vmRunning(name) {
state = "running"
}
osName := readFileTrim(filepath.Join(dir, "os.name"))
sshPort := readFileTrim(filepath.Join(dir, "ssh.port"))
vncPort := readFileTrim(filepath.Join(dir, "vnc.port"))
ssh := "-"
if sshPort != "-" {
ssh = "localhost:" + sshPort
}
vnc := "-"
if vncPort != "-" {
vnc = "0.0.0.0:" + vncPort
}
fmt.Printf("%-12s %-8s %-8s %-16s %-16s %s\n",
name, state, osName, ssh, vnc, vmPubKeyPath(name))
} }
} }
@ -319,28 +339,15 @@ func main() {
switch os.Args[1] { switch os.Args[1] {
case "create": case "create":
if len(os.Args) < 3 { createVM(os.Args[2], parseArg(os.Args, "--os"), parseArg(os.Args, "--pubkey-path"))
usage()
}
osName := parseArg(os.Args, "--os")
pubKey := parseArg(os.Args, "--pubkey-path")
createVM(os.Args[2], osName, pubKey)
case "start": case "start":
startVM(os.Args[2]) startVM(os.Args[2])
case "stop": case "stop":
stopVM(os.Args[2]) stopVM(os.Args[2])
case "delete": case "delete":
if len(os.Args) < 3 {
usage()
}
deleteVM(os.Args[2]) deleteVM(os.Args[2])
case "list": case "list":
listVMs() listVMs()
default: default:
usage() usage()
} }

BIN
microvm

Binary file not shown.