537 lines
12 KiB
Go
537 lines
12 KiB
Go
/**
|
|
* @author Mikhail Klementev jollheef<AT>riseup.net
|
|
* @license GNU GPLv3
|
|
* @date July 2018
|
|
* @brief appvm launcher
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"math/rand"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/digitalocean/go-libvirt"
|
|
"github.com/go-cmd/cmd"
|
|
"github.com/jollheef/go-system"
|
|
"github.com/olekukonko/tablewriter"
|
|
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
|
)
|
|
|
|
type networkModel int
|
|
|
|
const (
|
|
networkOffline networkModel = iota
|
|
networkQemu networkModel = iota
|
|
networkLibvirt networkModel = iota
|
|
)
|
|
|
|
func list(l *libvirt.Libvirt) {
|
|
domains, err := l.Domains()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
fmt.Println("Started VM:")
|
|
for _, d := range domains {
|
|
if strings.HasPrefix(d.Name, "appvm") {
|
|
fmt.Println("\t", d.Name[6:])
|
|
}
|
|
}
|
|
|
|
fmt.Println("\nAvailable VM:")
|
|
files, err := ioutil.ReadDir(configDir + "/nix")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
for _, f := range files {
|
|
switch f.Name() {
|
|
case "base.nix":
|
|
continue
|
|
case "local.nix":
|
|
continue
|
|
|
|
}
|
|
|
|
fmt.Println("\t", f.Name()[0:len(f.Name())-4])
|
|
}
|
|
}
|
|
|
|
func copyFile(from, to string) (err error) {
|
|
source, err := os.Open(from)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer source.Close()
|
|
|
|
destination, err := os.Create(to)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
_, err = io.Copy(destination, source)
|
|
if err != nil {
|
|
destination.Close()
|
|
return
|
|
}
|
|
|
|
return destination.Close()
|
|
}
|
|
|
|
func prepareTemplates(appvmPath string) (err error) {
|
|
if _, err = os.Stat(appvmPath + "/nix/local.nix"); os.IsNotExist(err) {
|
|
err = ioutil.WriteFile(configDir+"/nix/local.nix", local_nix_template, 0644)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func streamStdOutErr(command *cmd.Cmd) {
|
|
for {
|
|
select {
|
|
case line := <-command.Stdout:
|
|
fmt.Println(line)
|
|
case line := <-command.Stderr:
|
|
fmt.Fprintln(os.Stderr, line)
|
|
}
|
|
}
|
|
}
|
|
|
|
func generateVM(path, name string, verbose bool) (realpath, reginfo, qcow2 string, err error) {
|
|
command := cmd.NewCmdOptions(cmd.Options{Buffered: false, Streaming: true},
|
|
"nix-build", "<nixpkgs/nixos>", "-A", "config.system.build.vm",
|
|
"-I", "nixos-config="+path+"/nix/"+name+".nix", "-I", path)
|
|
|
|
if verbose {
|
|
go streamStdOutErr(command)
|
|
}
|
|
|
|
status := <-command.Start()
|
|
if status.Error != nil || status.Exit != 0 {
|
|
log.Println(status.Error, status.Stdout, status.Stderr)
|
|
if status.Error != nil {
|
|
err = status.Error
|
|
} else {
|
|
s := fmt.Sprintf("ret code: %d, out: %v, err: %v",
|
|
status.Exit, status.Stdout, status.Stderr)
|
|
err = errors.New(s)
|
|
}
|
|
return
|
|
}
|
|
|
|
realpath, err = filepath.EvalSymlinks("result/system")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
matches, err := filepath.Glob("result/bin/run-*-vm")
|
|
if err != nil || len(matches) != 1 {
|
|
return
|
|
}
|
|
|
|
bytes, err := ioutil.ReadFile(matches[0])
|
|
if err != nil || len(matches) != 1 {
|
|
return
|
|
}
|
|
|
|
match := regexp.MustCompile("regInfo=.*/registration").FindSubmatch(bytes)
|
|
if len(match) != 1 {
|
|
err = errors.New("should be one reginfo")
|
|
return
|
|
}
|
|
|
|
reginfo = string(match[0])
|
|
|
|
syscall.Unlink("result")
|
|
|
|
qcow2 = os.Getenv("HOME") + "/appvm/." + name + ".fake.qcow2"
|
|
if _, e := os.Stat(qcow2); os.IsNotExist(e) {
|
|
system.System("qemu-img", "create", "-f", "qcow2", qcow2, "40M")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func isRunning(l *libvirt.Libvirt, name string) bool {
|
|
_, err := l.DomainLookupByName("appvm_" + name) // yep, there is no libvirt error handling
|
|
// VM is destroyed when stop so NO VM means STOPPED
|
|
return err == nil
|
|
}
|
|
|
|
func generateAppVM(l *libvirt.Libvirt,
|
|
nixName, vmName, appvmPath, sharedDir string,
|
|
verbose bool, network networkModel, gui bool) (qcow2 string, err error) {
|
|
|
|
realpath, reginfo, qcow2, err := generateVM(appvmPath, nixName, verbose)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
xml := generateXML(vmName, network, gui, realpath, reginfo, qcow2, sharedDir)
|
|
_, err = l.DomainCreateXML(xml, libvirt.DomainStartValidate)
|
|
return
|
|
}
|
|
|
|
func stupidProgressBar() {
|
|
const length = 70
|
|
for {
|
|
time.Sleep(time.Second / 4)
|
|
fmt.Printf("\r%s]\r[", strings.Repeat(" ", length))
|
|
for i := 0; i <= length-2; i++ {
|
|
time.Sleep(time.Second / 20)
|
|
fmt.Printf("+")
|
|
}
|
|
}
|
|
}
|
|
|
|
func fileExists(filename string) bool {
|
|
info, err := os.Stat(filename)
|
|
if os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
return !info.IsDir()
|
|
}
|
|
|
|
func isAppvmConfigurationExists(appvmPath, name string) bool {
|
|
return fileExists(appvmPath + "/nix/" + name + ".nix")
|
|
}
|
|
|
|
func start(l *libvirt.Libvirt, name string, verbose bool, network networkModel,
|
|
gui, stateless bool, args, open string) {
|
|
|
|
appvmPath := configDir
|
|
|
|
statelessName := fmt.Sprintf("tmp_%d_%s", rand.Int(), name)
|
|
|
|
sharedDir := os.Getenv("HOME") + "/appvm/"
|
|
if stateless {
|
|
sharedDir += statelessName
|
|
} else {
|
|
sharedDir += name
|
|
}
|
|
|
|
os.MkdirAll(sharedDir, 0700)
|
|
|
|
vmName := "appvm_"
|
|
if stateless {
|
|
vmName += statelessName
|
|
} else {
|
|
vmName += name
|
|
}
|
|
|
|
if open != "" {
|
|
filename := sharedDir + "/" + filepath.Base(open)
|
|
err := copyFile(open, filename)
|
|
if err != nil {
|
|
log.Println("Can't copy file")
|
|
return
|
|
}
|
|
|
|
args += "/home/user/" + filepath.Base(open)
|
|
}
|
|
|
|
if args != "" {
|
|
err := ioutil.WriteFile(sharedDir+"/"+".args", []byte(args), 0700)
|
|
if err != nil {
|
|
log.Println("Can't write args")
|
|
return
|
|
}
|
|
}
|
|
|
|
if !isAppvmConfigurationExists(appvmPath, name) {
|
|
log.Println("No configuration exists for app, " +
|
|
"trying to generate")
|
|
err := generate(name, "", "", false)
|
|
if err != nil {
|
|
log.Println("Can't auto generate")
|
|
return
|
|
}
|
|
}
|
|
|
|
if !isRunning(l, vmName) {
|
|
if !verbose {
|
|
go stupidProgressBar()
|
|
}
|
|
|
|
qcow2, err := generateAppVM(l, name, vmName, appvmPath, sharedDir,
|
|
verbose, network, gui)
|
|
defer os.Remove(qcow2)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
if gui {
|
|
cmd := exec.Command("virt-viewer", "-c", "qemu:///system", vmName)
|
|
cmd.Start()
|
|
}
|
|
}
|
|
|
|
func stop(l *libvirt.Libvirt, name string) {
|
|
dom, err := l.DomainLookupByName("appvm_" + name)
|
|
if err != nil {
|
|
if libvirt.IsNotFound(err) {
|
|
log.Println("Appvm not found or already stopped")
|
|
return
|
|
} else {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
err = l.DomainShutdown(dom)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func drop(name string) {
|
|
appDataPath := fmt.Sprintf(os.Getenv("HOME") + "/appvm/" + name)
|
|
os.RemoveAll(appDataPath)
|
|
}
|
|
|
|
func autoBalloon(l *libvirt.Libvirt, memoryMin, adjustPercent uint64) {
|
|
domains, err := l.Domains()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
table := tablewriter.NewWriter(os.Stdout)
|
|
table.SetHeader([]string{"Application VM", "Used memory", "Current memory", "Max memory", "New memory"})
|
|
for _, d := range domains {
|
|
if strings.HasPrefix(d.Name, "appvm_") {
|
|
name := d.Name[6:]
|
|
|
|
memoryUsedRaw, err := ioutil.ReadFile(os.Getenv("HOME") + "/appvm/" + name + "/.memory_used")
|
|
if err != nil {
|
|
log.Println(err)
|
|
continue
|
|
}
|
|
if len(memoryUsedRaw) == 0 {
|
|
log.Println("Empty .memory_used file for domain", name)
|
|
continue
|
|
}
|
|
memoryUsedMiB, err := strconv.Atoi(string(memoryUsedRaw[0 : len(memoryUsedRaw)-1]))
|
|
if err != nil {
|
|
log.Println(err)
|
|
continue
|
|
}
|
|
memoryUsed := memoryUsedMiB * 1024
|
|
|
|
_, memoryMax, memoryCurrent, _, _, err := l.DomainGetInfo(d)
|
|
if err != nil {
|
|
log.Println(err)
|
|
continue
|
|
}
|
|
|
|
memoryNew := uint64(float64(memoryUsed) * (1 + float64(adjustPercent)/100))
|
|
|
|
if memoryNew > memoryMax {
|
|
memoryNew = memoryMax - 1
|
|
}
|
|
|
|
if memoryNew < memoryMin {
|
|
memoryNew = memoryMin
|
|
}
|
|
|
|
err = l.DomainSetMemory(d, memoryNew)
|
|
if err != nil {
|
|
log.Println(err)
|
|
continue
|
|
}
|
|
|
|
table.Append([]string{name,
|
|
fmt.Sprintf("%d", memoryUsed),
|
|
fmt.Sprintf("%d", memoryCurrent),
|
|
fmt.Sprintf("%d", memoryMax),
|
|
fmt.Sprintf("%d", memoryNew)})
|
|
}
|
|
}
|
|
table.Render()
|
|
}
|
|
|
|
func search(name string) {
|
|
command := exec.Command("nix", "search", name)
|
|
bytes, err := command.Output()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, line := range strings.Split(string(bytes), "\n") {
|
|
fmt.Println(line)
|
|
}
|
|
return
|
|
}
|
|
|
|
func sync() {
|
|
err := exec.Command("nix-channel", "--update").Run()
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
err = exec.Command("nix", "search", "-u").Run()
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
log.Println("Done")
|
|
}
|
|
|
|
func cleanupStatelessVMs(l *libvirt.Libvirt) {
|
|
domains, err := l.Domains()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
dirs, err := ioutil.ReadDir(appvmHomesDir)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
for _, f := range dirs {
|
|
if !strings.HasPrefix(f.Name(), "tmp_") {
|
|
continue
|
|
}
|
|
|
|
alive := false
|
|
for _, d := range domains {
|
|
if d.Name == "appvm_"+f.Name() {
|
|
alive = true
|
|
}
|
|
}
|
|
if !alive {
|
|
os.RemoveAll(appvmHomesDir + f.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
func parseNetworkModel(flagOffline bool, flagNetworking string) networkModel {
|
|
if flagNetworking != "" && flagOffline {
|
|
log.Fatal("Can't use both --network and --offline switches")
|
|
}
|
|
if flagOffline || flagNetworking == "offline" {
|
|
return networkOffline
|
|
}
|
|
if flagNetworking == "libvirt" {
|
|
return networkLibvirt
|
|
}
|
|
if flagNetworking == "qemu" {
|
|
return networkQemu
|
|
}
|
|
return networkQemu // qemu is the default network model
|
|
}
|
|
|
|
var configDir = os.Getenv("HOME") + "/.config/appvm/"
|
|
var appvmHomesDir = os.Getenv("HOME") + "/appvm/"
|
|
|
|
func main() {
|
|
rand.Seed(time.Now().UnixNano())
|
|
|
|
os.Mkdir(os.Getenv("HOME")+"/appvm", 0700)
|
|
|
|
os.MkdirAll(configDir+"/nix", 0700)
|
|
|
|
err := writeBuiltinApps(configDir + "/nix")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
err = ioutil.WriteFile(configDir+"/nix/base.nix", baseNix(), 0644)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Copy templates
|
|
err = prepareTemplates(configDir)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
kingpin.Command("list", "List applications")
|
|
autoballonCommand := kingpin.Command("autoballoon", "Automatically adjust/reduce app vm memory")
|
|
minMemory := autoballonCommand.Flag("min-memory", "Set minimal memory (megabytes)").Default("1024").Uint64()
|
|
adjustPercent := autoballonCommand.Flag("adj-memory", "Adjust memory amount (percents)").Default("20").Uint64()
|
|
|
|
startCommand := kingpin.Command("start", "Start application")
|
|
startName := startCommand.Arg("name", "Application name").Required().String()
|
|
startQuiet := startCommand.Flag("quiet", "Less verbosity").Bool()
|
|
startArgs := startCommand.Flag("args", "Command line arguments").String()
|
|
startOpen := startCommand.Flag("open", "Pass file to application").String()
|
|
startOffline := startCommand.Flag("offline", "Disconnect").Bool()
|
|
startCli := startCommand.Flag("cli", "Disable graphics mode, enable serial").Bool()
|
|
startStateless := startCommand.Flag("stateless", "Do not use default state directory").Bool()
|
|
startNetwork := startCommand.Flag("network", "Used networking model").Enum("offline", "qemu", "libvirt")
|
|
|
|
stopName := kingpin.Command("stop", "Stop application").Arg("name", "Application name").Required().String()
|
|
dropName := kingpin.Command("drop", "Remove application data").Arg("name", "Application name").Required().String()
|
|
|
|
generateCommand := kingpin.Command("generate", "Generate appvm definition")
|
|
generateName := generateCommand.Arg("name", "Nix package name").Required().String()
|
|
generateBin := generateCommand.Arg("bin", "Binary").Default("").String()
|
|
generateVMName := generateCommand.Flag("vm", "Use VM Name").Default("").String()
|
|
generateBuildVM := generateCommand.Flag("build", "Build VM").Bool()
|
|
|
|
searchCommand := kingpin.Command("search", "Search for application")
|
|
searchName := searchCommand.Arg("name", "Application name").Required().String()
|
|
|
|
kingpin.Command("sync", "Synchronize remote repos for applications")
|
|
|
|
var l *libvirt.Libvirt
|
|
if kingpin.Parse() != "generate" {
|
|
c, err := net.DialTimeout(
|
|
"unix",
|
|
"/var/run/libvirt/libvirt-sock",
|
|
time.Second,
|
|
)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
l = libvirt.New(c)
|
|
if err := l.Connect(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer l.Disconnect()
|
|
|
|
cleanupStatelessVMs(l)
|
|
}
|
|
|
|
switch kingpin.Parse() {
|
|
case "list":
|
|
list(l)
|
|
case "search":
|
|
search(*searchName)
|
|
case "generate":
|
|
generate(*generateName, *generateBin, *generateVMName,
|
|
*generateBuildVM)
|
|
case "start":
|
|
networkModel := parseNetworkModel(*startOffline, *startNetwork)
|
|
start(l, *startName,
|
|
!*startQuiet, networkModel, !*startCli, *startStateless,
|
|
*startArgs, *startOpen)
|
|
case "stop":
|
|
stop(l, *stopName)
|
|
case "drop":
|
|
drop(*dropName)
|
|
case "autoballoon":
|
|
autoBalloon(l, *minMemory*1024, *adjustPercent)
|
|
case "sync":
|
|
sync()
|
|
}
|
|
}
|