renamed to mnmivm, more detailed list view
This commit is contained in:
parent
1e7135b2d2
commit
010d78a1cc
3 changed files with 260 additions and 253 deletions
2
build.sh
2
build.sh
|
|
@ -1,2 +1,2 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
go build -o ./microvm
|
go build -o ./mnmivm
|
||||||
|
|
|
||||||
203
main.go
203
main.go
|
|
@ -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
BIN
microvm
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue