2025-12-15 13:16:09 -05:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2025-12-15 15:10:22 -05:00
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net"
|
2025-12-15 16:28:30 -05:00
|
|
|
"time"
|
2025-12-15 15:10:22 -05:00
|
|
|
"net/http"
|
|
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
2025-12-15 13:16:09 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
2025-12-15 15:10:22 -05:00
|
|
|
rootDir = "/var/lib/microvm"
|
|
|
|
|
imageDir = rootDir + "/images"
|
|
|
|
|
vmDirBase = rootDir + "/vms"
|
2025-12-15 13:16:09 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
memMB = "1024"
|
|
|
|
|
cpus = "1"
|
2025-12-15 13:16:09 -05:00
|
|
|
)
|
|
|
|
|
|
2025-12-15 13:59:20 -05:00
|
|
|
type OSImage struct {
|
2025-12-15 15:10:22 -05:00
|
|
|
Name string
|
|
|
|
|
URL string
|
|
|
|
|
Filename string
|
|
|
|
|
User string
|
2025-12-15 13:59:20 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var osImages = map[string]OSImage{
|
2025-12-15 15:10:22 -05:00
|
|
|
"ubuntu": {
|
|
|
|
|
Name: "ubuntu",
|
|
|
|
|
URL: "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img",
|
|
|
|
|
Filename: "ubuntu-24.04.qcow2",
|
|
|
|
|
User: "ubuntu",
|
|
|
|
|
},
|
|
|
|
|
"debian": {
|
|
|
|
|
Name: "debian",
|
|
|
|
|
URL: "https://cloud.debian.org/images/cloud/trixie/20251117-2299/debian-13-generic-amd64-20251117-2299.qcow2",
|
|
|
|
|
Filename: "debian-13.qcow2",
|
|
|
|
|
User: "debian",
|
|
|
|
|
},
|
|
|
|
|
"fedora": {
|
|
|
|
|
Name: "fedora",
|
|
|
|
|
URL: "https://download.fedoraproject.org/pub/fedora/linux/releases/43/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-43-1.6.x86_64.qcow2",
|
|
|
|
|
Filename: "fedora-43.qcow2",
|
|
|
|
|
User: "fedora",
|
|
|
|
|
},
|
|
|
|
|
"alpine": {
|
|
|
|
|
Name: "alpine",
|
|
|
|
|
URL: "https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/cloud/generic_alpine-3.22.2-x86_64-bios-cloudinit-r0.qcow2",
|
|
|
|
|
Filename: "alpine-3.22.qcow2",
|
|
|
|
|
User: "alpine",
|
|
|
|
|
},
|
2025-12-15 13:59:20 -05:00
|
|
|
}
|
|
|
|
|
|
2025-12-15 16:01:02 -05:00
|
|
|
func requireRoot() {
|
|
|
|
|
if os.Geteuid() != 0 {
|
|
|
|
|
fmt.Fprintln(os.Stderr, "mnmivm must be run as root. Try: `sudo mnmivm` or login as the root user")
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-15 13:16:09 -05:00
|
|
|
func ensureDirs() {
|
2025-12-15 15:10:22 -05:00
|
|
|
for _, d := range []string{imageDir, vmDirBase} {
|
|
|
|
|
if err := os.MkdirAll(d, 0755); err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-15 13:16:09 -05:00
|
|
|
}
|
|
|
|
|
|
2025-12-15 13:59:20 -05:00
|
|
|
func usage() {
|
2025-12-15 15:10:22 -05:00
|
|
|
fmt.Println("usage:")
|
|
|
|
|
fmt.Println(" mnmivm create <name> --os <ubuntu|debian|fedora|alpine> --pubkey-path <path>")
|
|
|
|
|
fmt.Println(" mnmivm start <name>")
|
|
|
|
|
fmt.Println(" mnmivm stop <name>")
|
2025-12-15 16:28:30 -05:00
|
|
|
fmt.Println(" mnmivm update-cloud <name> --pubkey-path <path>")
|
2025-12-15 15:10:22 -05:00
|
|
|
fmt.Println(" mnmivm delete <name>")
|
|
|
|
|
fmt.Println(" mnmivm list")
|
|
|
|
|
os.Exit(1)
|
2025-12-15 13:16:09 -05:00
|
|
|
}
|
|
|
|
|
|
2025-12-15 13:59:20 -05:00
|
|
|
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 vmPubKeyPath(name string) string { return filepath.Join(vmDir(name), "pubkey.pub") }
|
|
|
|
|
|
|
|
|
|
func imagePath(osName string) string {
|
2025-12-15 15:10:22 -05:00
|
|
|
return filepath.Join(imageDir, osImages[osName].Filename)
|
2025-12-15 13:16:09 -05:00
|
|
|
}
|
2025-12-15 13:59:20 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
func parseArg(args []string, key string) string {
|
|
|
|
|
for i := 0; i < len(args)-1; i++ {
|
|
|
|
|
if args[i] == key {
|
|
|
|
|
return args[i+1]
|
2025-12-15 13:59:20 -05:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-15 15:10:22 -05:00
|
|
|
panic("missing required argument: " + key)
|
|
|
|
|
}
|
2025-12-15 13:59:20 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
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
|
|
|
|
|
}
|
2025-12-15 13:59:20 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
func ensureBaseImage(osName string) {
|
|
|
|
|
img := osImages[osName]
|
|
|
|
|
path := imagePath(osName)
|
2025-12-15 13:59:20 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
if _, err := os.Stat(path); err == nil {
|
2025-12-15 13:59:20 -05:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
fmt.Println("Downloading", img.Name, "cloud image...")
|
|
|
|
|
resp, err := http.Get(img.URL)
|
|
|
|
|
if err != nil {
|
2025-12-15 13:59:20 -05:00
|
|
|
panic(err)
|
|
|
|
|
}
|
2025-12-15 15:10:22 -05:00
|
|
|
defer resp.Body.Close()
|
2025-12-15 13:59:20 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
out, err := os.Create(path)
|
2025-12-15 14:35:36 -05:00
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
2025-12-15 15:10:22 -05:00
|
|
|
defer out.Close()
|
2025-12-15 14:35:36 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
if _, err := io.Copy(out, resp.Body); err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
2025-12-15 13:59:20 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func readPublicKey(path string) string {
|
2025-12-15 15:10:22 -05:00
|
|
|
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
|
2025-12-15 13:16:09 -05:00
|
|
|
}
|
|
|
|
|
|
2025-12-15 16:28:30 -05:00
|
|
|
func updateCloudVM(name, pubKeyPath string) {
|
|
|
|
|
dir := vmDir(name)
|
|
|
|
|
|
|
|
|
|
if _, err := os.Stat(dir); err != nil {
|
|
|
|
|
panic("VM does not exist: " + name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if vmRunning(name) {
|
|
|
|
|
panic("VM is currently running. Stop it before updating cloud-init.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
osName := readFileTrim(filepath.Join(dir, "os.name"))
|
|
|
|
|
if osName == "-" {
|
|
|
|
|
panic("cannot determine OS for VM")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// overwrite stored pubkey
|
|
|
|
|
keyData, err := os.ReadFile(pubKeyPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := os.WriteFile(vmPubKeyPath(name), keyData, 0644); err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// regenerate seed ISO
|
|
|
|
|
createCloudInitSeed(name, osName, vmPubKeyPath(name))
|
|
|
|
|
|
|
|
|
|
fmt.Println("Cloud-init updated for VM:", name)
|
|
|
|
|
fmt.Println("Changes will apply on next boot.")
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-15 13:59:20 -05:00
|
|
|
func createCloudInitSeed(name, osName, pubKeyPath string) {
|
2025-12-15 15:10:22 -05:00
|
|
|
img := osImages[osName]
|
|
|
|
|
sshKey := readPublicKey(pubKeyPath)
|
2025-12-15 13:16:09 -05:00
|
|
|
|
2025-12-15 16:28:30 -05:00
|
|
|
instanceID := fmt.Sprintf("%s-%d", name, time.Now().Unix())
|
|
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
userData := fmt.Sprintf(`#cloud-config
|
2025-12-15 13:16:09 -05:00
|
|
|
users:
|
2025-12-15 13:59:20 -05:00
|
|
|
- name: %s
|
2025-12-15 13:16:09 -05:00
|
|
|
sudo: ALL=(ALL) NOPASSWD:ALL
|
2025-12-15 13:59:20 -05:00
|
|
|
shell: /bin/sh
|
2025-12-15 17:05:24 -05:00
|
|
|
lock_passwd: true
|
|
|
|
|
create_home: true
|
2025-12-15 16:28:30 -05:00
|
|
|
|
2025-12-15 13:16:09 -05:00
|
|
|
ssh_pwauth: false
|
|
|
|
|
disable_root: true
|
|
|
|
|
|
2025-12-15 16:28:30 -05:00
|
|
|
runcmd:
|
2025-12-15 17:05:24 -05:00
|
|
|
- mkdir -p /home/%s/.ssh
|
|
|
|
|
- echo '%s' > /home/%s/.ssh/authorized_keys
|
2025-12-15 16:28:30 -05:00
|
|
|
- chown -R %s:%s /home/%s/.ssh
|
2025-12-15 17:05:24 -05:00
|
|
|
- chmod 700 /home/%s/.ssh
|
|
|
|
|
- chmod 600 /home/%s/.ssh/authorized_keys
|
|
|
|
|
`, img.User,
|
|
|
|
|
img.User,
|
|
|
|
|
sshKey,
|
|
|
|
|
img.User,
|
|
|
|
|
img.User, img.User,
|
|
|
|
|
img.User,
|
|
|
|
|
img.User,
|
|
|
|
|
img.User)
|
2025-12-15 16:28:30 -05:00
|
|
|
|
|
|
|
|
metaData := fmt.Sprintf(
|
|
|
|
|
"instance-id: %s\nlocal-hostname: %s\n",
|
|
|
|
|
instanceID,
|
|
|
|
|
name,
|
|
|
|
|
)
|
2025-12-15 15:10:22 -05:00
|
|
|
|
2025-12-15 16:28:30 -05:00
|
|
|
tmpDir, err := os.MkdirTemp("", "cloudinit")
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
2025-12-15 15:10:22 -05:00
|
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
|
|
2025-12-15 17:05:24 -05:00
|
|
|
if err := os.WriteFile(filepath.Join(tmpDir, "user-data"), []byte(userData), 0644); err != nil {
|
2025-12-15 16:28:30 -05:00
|
|
|
panic(err)
|
|
|
|
|
}
|
2025-12-15 17:05:24 -05:00
|
|
|
if err := os.WriteFile(filepath.Join(tmpDir, "meta-data"), []byte(metaData), 0644); err != nil {
|
2025-12-15 16:28:30 -05:00
|
|
|
panic(err)
|
|
|
|
|
}
|
2025-12-15 15:10:22 -05:00
|
|
|
|
|
|
|
|
cmd := exec.Command(
|
|
|
|
|
"genisoimage",
|
|
|
|
|
"-output", seedISOPath(name),
|
|
|
|
|
"-volid", "cidata",
|
|
|
|
|
"-joliet",
|
|
|
|
|
"-rock",
|
2025-12-15 17:05:24 -05:00
|
|
|
filepath.Join(tmpDir, "user-data"),
|
|
|
|
|
filepath.Join(tmpDir, "meta-data"),
|
2025-12-15 15:10:22 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
2025-12-15 16:28:30 -05:00
|
|
|
panic("cloud-init ISO creation failed:\n" + string(out))
|
2025-12-15 15:10:22 -05:00
|
|
|
}
|
2025-12-15 13:16:09 -05:00
|
|
|
}
|
|
|
|
|
|
2025-12-15 16:28:30 -05:00
|
|
|
|
2025-12-15 17:05:24 -05:00
|
|
|
|
2025-12-15 16:01:02 -05:00
|
|
|
func renderBoxTable(headers []string, rows [][]string) {
|
|
|
|
|
colWidths := make([]int, len(headers))
|
|
|
|
|
|
|
|
|
|
// compute column widths
|
|
|
|
|
for i, h := range headers {
|
|
|
|
|
colWidths[i] = len(h)
|
|
|
|
|
}
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
for i, cell := range row {
|
|
|
|
|
if len(cell) > colWidths[i] {
|
|
|
|
|
colWidths[i] = len(cell)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// helpers
|
|
|
|
|
repeat := func(s string, n int) string {
|
|
|
|
|
return strings.Repeat(s, n)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drawRow := func(left, mid, right string) {
|
|
|
|
|
fmt.Print(left)
|
|
|
|
|
for i, w := range colWidths {
|
|
|
|
|
fmt.Print(repeat("─", w+2))
|
|
|
|
|
if i < len(colWidths)-1 {
|
|
|
|
|
fmt.Print(mid)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fmt.Println(right)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// top border
|
|
|
|
|
drawRow("┌", "┬", "┐")
|
|
|
|
|
|
|
|
|
|
// header row
|
|
|
|
|
fmt.Print("│")
|
|
|
|
|
for i, h := range headers {
|
|
|
|
|
fmt.Printf(" %-*s │", colWidths[i], h)
|
|
|
|
|
}
|
|
|
|
|
fmt.Println()
|
|
|
|
|
|
|
|
|
|
// header separator
|
|
|
|
|
drawRow("├", "┼", "┤")
|
|
|
|
|
|
|
|
|
|
// data rows
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
fmt.Print("│")
|
|
|
|
|
for i, cell := range row {
|
|
|
|
|
fmt.Printf(" %-*s │", colWidths[i], cell)
|
|
|
|
|
}
|
|
|
|
|
fmt.Println()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// bottom border
|
|
|
|
|
drawRow("└", "┴", "┘")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-12-15 13:59:20 -05:00
|
|
|
func createVM(name, osName, pubKeyPath string) {
|
2025-12-15 15:10:22 -05:00
|
|
|
if _, ok := osImages[osName]; !ok {
|
|
|
|
|
panic("unsupported OS: " + osName)
|
|
|
|
|
}
|
2025-12-15 13:16:09 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
ensureBaseImage(osName)
|
|
|
|
|
os.MkdirAll(vmDir(name), 0755)
|
2025-12-15 13:59:20 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
keyData, _ := os.ReadFile(pubKeyPath)
|
|
|
|
|
os.WriteFile(vmPubKeyPath(name), keyData, 0644)
|
|
|
|
|
os.WriteFile(filepath.Join(vmDir(name), "os.name"), []byte(osName), 0644)
|
2025-12-15 13:16:09 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
cmd := exec.Command(
|
|
|
|
|
"qemu-img", "create",
|
|
|
|
|
"-f", "qcow2",
|
|
|
|
|
"-F", "qcow2",
|
|
|
|
|
"-b", imagePath(osName),
|
|
|
|
|
diskPath(name),
|
|
|
|
|
)
|
2025-12-15 13:16:09 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
|
|
|
panic(string(out))
|
|
|
|
|
}
|
2025-12-15 13:16:09 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
createCloudInitSeed(name, osName, vmPubKeyPath(name))
|
|
|
|
|
fmt.Println("VM created:", name, "OS:", osName)
|
2025-12-15 13:16:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func startVM(name string) {
|
2025-12-15 15:10:22 -05:00
|
|
|
if data, err := os.ReadFile(pidFile(name)); err == nil {
|
|
|
|
|
if _, err := os.Stat("/proc/" + strings.TrimSpace(string(data))); err == nil {
|
|
|
|
|
panic("VM already running")
|
|
|
|
|
}
|
|
|
|
|
os.Remove(pidFile(name))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sshPort := findFreePort()
|
|
|
|
|
vncPort := findFreePort()
|
|
|
|
|
vncDisplay := vncPort - 5900
|
|
|
|
|
if vncDisplay < 0 {
|
|
|
|
|
panic("invalid VNC port")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := exec.Command(
|
|
|
|
|
"qemu-system-x86_64",
|
|
|
|
|
"-enable-kvm",
|
|
|
|
|
"-machine", "q35",
|
|
|
|
|
"-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", sshPort),
|
|
|
|
|
"-device", "virtio-net-pci,netdev=net0",
|
|
|
|
|
"-pidfile", pidFile(name),
|
|
|
|
|
"-daemonize",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
|
|
|
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("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)
|
2025-12-15 13:16:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func stopVM(name string) {
|
2025-12-15 15:10:22 -05:00
|
|
|
data, _ := os.ReadFile(pidFile(name))
|
|
|
|
|
exec.Command("kill", strings.TrimSpace(string(data))).Run()
|
|
|
|
|
os.Remove(pidFile(name))
|
|
|
|
|
fmt.Println("VM stopped:", name)
|
2025-12-15 13:16:09 -05:00
|
|
|
}
|
|
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
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)
|
2025-12-15 13:16:09 -05:00
|
|
|
}
|
|
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
func readFileTrim(path string) string {
|
|
|
|
|
data, err := os.ReadFile(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "-"
|
|
|
|
|
}
|
|
|
|
|
return strings.TrimSpace(string(data))
|
|
|
|
|
}
|
2025-12-15 13:16:09 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
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
|
|
|
|
|
}
|
2025-12-15 13:16:09 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
func listVMs() {
|
|
|
|
|
entries, _ := os.ReadDir(vmDirBase)
|
2025-12-15 13:59:20 -05:00
|
|
|
|
2025-12-15 16:01:02 -05:00
|
|
|
headers := []string{
|
|
|
|
|
"NAME",
|
|
|
|
|
"STATE",
|
|
|
|
|
"OS",
|
|
|
|
|
"SSH",
|
|
|
|
|
"VNC",
|
|
|
|
|
"PUBKEY",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rows := [][]string{}
|
2025-12-15 13:59:20 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
for _, e := range entries {
|
|
|
|
|
name := e.Name()
|
|
|
|
|
dir := vmDir(name)
|
2025-12-15 13:59:20 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
state := "stopped"
|
|
|
|
|
if vmRunning(name) {
|
|
|
|
|
state = "running"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
osName := readFileTrim(filepath.Join(dir, "os.name"))
|
2025-12-15 16:01:02 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-15 16:01:02 -05:00
|
|
|
rows = append(rows, []string{
|
|
|
|
|
name,
|
|
|
|
|
state,
|
|
|
|
|
osName,
|
|
|
|
|
ssh,
|
|
|
|
|
vnc,
|
|
|
|
|
vmPubKeyPath(name),
|
|
|
|
|
})
|
2025-12-15 13:59:20 -05:00
|
|
|
}
|
2025-12-15 16:01:02 -05:00
|
|
|
|
|
|
|
|
if len(rows) == 0 {
|
|
|
|
|
fmt.Println("No VMs found.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderBoxTable(headers, rows)
|
2025-12-15 15:10:22 -05:00
|
|
|
}
|
2025-12-15 13:59:20 -05:00
|
|
|
|
2025-12-15 16:01:02 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
func main() {
|
2025-12-15 16:01:02 -05:00
|
|
|
requireRoot()
|
2025-12-15 15:10:22 -05:00
|
|
|
if len(os.Args) < 2 {
|
|
|
|
|
usage()
|
|
|
|
|
}
|
2025-12-15 13:59:20 -05:00
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
ensureDirs()
|
|
|
|
|
|
|
|
|
|
switch os.Args[1] {
|
|
|
|
|
case "create":
|
|
|
|
|
createVM(os.Args[2], parseArg(os.Args, "--os"), parseArg(os.Args, "--pubkey-path"))
|
2025-12-15 17:05:24 -05:00
|
|
|
updateCloudVM(os.Args[2], parseArg(os.Args, "--pubkey-path"))
|
2025-12-15 15:10:22 -05:00
|
|
|
case "start":
|
|
|
|
|
startVM(os.Args[2])
|
|
|
|
|
case "stop":
|
|
|
|
|
stopVM(os.Args[2])
|
|
|
|
|
case "delete":
|
|
|
|
|
deleteVM(os.Args[2])
|
|
|
|
|
case "list":
|
|
|
|
|
listVMs()
|
2025-12-15 16:28:30 -05:00
|
|
|
case "update-cloud":
|
|
|
|
|
if len(os.Args) < 3 {
|
|
|
|
|
usage()
|
|
|
|
|
}
|
|
|
|
|
updateCloudVM(os.Args[2], parseArg(os.Args, "--pubkey-path"))
|
|
|
|
|
|
2025-12-15 15:10:22 -05:00
|
|
|
default:
|
|
|
|
|
usage()
|
|
|
|
|
}
|
2025-12-15 13:16:09 -05:00
|
|
|
}
|
|
|
|
|
|