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
go build -o ./microvm
go build -o ./mnmivm

203
main.go
View file

@ -3,8 +3,8 @@ package main
import (
"fmt"
"io"
"net/http"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
@ -64,11 +64,11 @@ func ensureDirs() {
func usage() {
fmt.Println("usage:")
fmt.Println(" microvm create <name> --os <ubuntu|debian|fedora|alpine> --pubkey-path <path>")
fmt.Println(" microvm start <name>")
fmt.Println(" microvm stop <name>")
fmt.Println(" microvm delete <name>")
fmt.Println(" microvm list")
fmt.Println(" mnmivm create <name> --os <ubuntu|debian|fedora|alpine> --pubkey-path <path>")
fmt.Println(" mnmivm start <name>")
fmt.Println(" mnmivm stop <name>")
fmt.Println(" mnmivm delete <name>")
fmt.Println(" mnmivm list")
os.Exit(1)
}
@ -82,55 +82,6 @@ func imagePath(osName string) string {
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 {
for i := 0; i < len(args)-1; i++ {
if args[i] == key {
@ -140,16 +91,13 @@ func parseArg(args []string, key string) string {
panic("missing required argument: " + key)
}
func readPublicKey(path string) string {
data, err := os.ReadFile(path)
func findFreePort() int {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(err)
}
key := strings.TrimSpace(string(data))
if !strings.HasPrefix(key, "ssh-") {
panic("invalid SSH public key")
}
return key
defer l.Close()
return l.Addr().(*net.TCPAddr).Port
}
func ensureBaseImage(osName string) {
@ -161,7 +109,6 @@ func ensureBaseImage(osName string) {
}
fmt.Println("Downloading", img.Name, "cloud image...")
resp, err := http.Get(img.URL)
if err != nil {
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) {
img := osImages[osName]
sshKey := readPublicKey(pubKeyPath)
@ -190,14 +149,11 @@ users:
shell: /bin/sh
ssh_authorized_keys:
- %s
ssh_pwauth: false
disable_root: true
`, img.User, sshKey)
metaData := fmt.Sprintf(`instance-id: %s
local-hostname: %s
`, name, name)
metaData := fmt.Sprintf("instance-id: %s\nlocal-hostname: %s\n", name, name)
tmpDir, _ := os.MkdirTemp("", "cloudinit")
defer os.RemoveAll(tmpDir)
@ -228,9 +184,9 @@ func createVM(name, osName, pubKeyPath string) {
ensureBaseImage(osName)
os.MkdirAll(vmDir(name), 0755)
keyDst := vmPubKeyPath(name)
data, _ := os.ReadFile(pubKeyPath)
os.WriteFile(keyDst, data, 0644)
keyData, _ := os.ReadFile(pubKeyPath)
os.WriteFile(vmPubKeyPath(name), keyData, 0644)
os.WriteFile(filepath.Join(vmDir(name), "os.name"), []byte(osName), 0644)
cmd := exec.Command(
"qemu-img", "create",
@ -244,8 +200,7 @@ func createVM(name, osName, pubKeyPath string) {
panic(string(out))
}
createCloudInitSeed(name, osName, keyDst)
createCloudInitSeed(name, osName, vmPubKeyPath(name))
fmt.Println("VM created:", name, "OS:", osName)
}
@ -257,14 +212,13 @@ func startVM(name string) {
os.Remove(pidFile(name))
}
port := findFreePort()
sshPort := findFreePort()
vncPort := findFreePort()
vncDisplay := vncPort - 5900
if vncDisplay < 0 {
panic("invalid VNC port allocation")
panic("invalid VNC port")
}
cmd := exec.Command(
"qemu-system-x86_64",
"-enable-kvm",
@ -272,17 +226,12 @@ func startVM(name string) {
"-cpu", "host",
"-m", memMB,
"-smp", cpus,
"-vga", "virtio",
"-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=raw", seedISOPath(name)),
"-netdev", fmt.Sprintf("user,id=net0,hostfwd=tcp::%d-:22", port),
"-netdev", fmt.Sprintf("user,id=net0,hostfwd=tcp::%d-:22", sshPort),
"-device", "virtio-net-pci,netdev=net0",
"-boot", "order=c",
"-pidfile", pidFile(name),
"-daemonize",
)
@ -291,9 +240,12 @@ func startVM(name string) {
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.Printf("VNC: vnc://127.0.0.1:%d\n", vncPort)
fmt.Printf("SSH: ssh <user>@localhost -p %d\n", port)
fmt.Printf("SSH: ssh %s@localhost -p %d\n", osImages[readFileTrim(filepath.Join(vmDir(name), "os.name"))].User, sshPort)
fmt.Printf("VNC: vnc://0.0.0.0:%d\n", vncPort)
}
func stopVM(name string) {
@ -303,10 +255,78 @@ func stopVM(name string) {
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() {
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 {
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] {
case "create":
if len(os.Args) < 3 {
usage()
}
osName := parseArg(os.Args, "--os")
pubKey := parseArg(os.Args, "--pubkey-path")
createVM(os.Args[2], osName, pubKey)
createVM(os.Args[2], parseArg(os.Args, "--os"), parseArg(os.Args, "--pubkey-path"))
case "start":
startVM(os.Args[2])
case "stop":
stopVM(os.Args[2])
case "delete":
if len(os.Args) < 3 {
usage()
}
deleteVM(os.Args[2])
case "list":
listVMs()
default:
usage()
}

BIN
microvm

Binary file not shown.