appvm/appvm.go

443 lines
11 KiB
Go
Raw Normal View History

2018-07-11 23:34:36 +00:00
/**
* @author Mikhail Klementev jollheef<AT>riseup.net
* @license GNU GPLv3
* @date July 2018
* @brief appvm launcher
*/
package main
import (
2019-05-12 00:10:25 +00:00
"errors"
2018-07-11 23:34:36 +00:00
"fmt"
"io"
2018-07-12 00:06:43 +00:00
"io/ioutil"
2018-07-11 23:34:36 +00:00
"log"
"net"
"os"
"os/exec"
"path/filepath"
2019-12-29 07:24:59 +00:00
"regexp"
2018-07-12 20:00:57 +00:00
"strconv"
2018-07-15 19:54:38 +00:00
"strings"
2018-07-11 23:34:36 +00:00
"syscall"
"time"
"github.com/digitalocean/go-libvirt"
2018-08-04 09:27:53 +00:00
"github.com/go-cmd/cmd"
2018-07-11 23:34:36 +00:00
"github.com/jollheef/go-system"
2018-07-12 20:00:57 +00:00
"github.com/olekukonko/tablewriter"
2018-07-11 23:34:36 +00:00
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
var xmlTmpl = `
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
<name>%s</name>
2018-07-12 19:16:34 +00:00
<memory unit='GiB'>2</memory>
<currentMemory unit='GiB'>1</currentMemory>
<vcpu>4</vcpu>
2018-07-11 23:34:36 +00:00
<os>
<type arch='x86_64' machine='pc-i440fx-2.12'>hvm</type>
<kernel>%s/kernel</kernel>
<initrd>%s/initrd</initrd>
<cmdline>loglevel=4 init=%s/init %s</cmdline>
</os>
<features>
<acpi/>
</features>
<clock offset='utc'/>
<on_poweroff>destroy</on_poweroff>
<on_reboot>restart</on_reboot>
<on_crash>destroy</on_crash>
<devices>
<!-- Graphical console -->
<graphics type='spice' autoport='yes'>
<listen type='address'/>
<image compression='off'/>
</graphics>
2018-07-12 17:43:42 +00:00
<!-- Guest additionals support -->
<channel type='spicevmc'>
<target type='virtio' name='com.redhat.spice.0'/>
</channel>
2018-07-11 23:34:36 +00:00
<!-- Fake (because -snapshot) writeback image -->
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2' cache='writeback' error_policy='report'/>
<source file='%s'/>
<target dev='vda' bus='virtio'/>
</disk>
<video>
<model type='qxl' ram='524288' vram='524288' vgamem='262144' heads='1' primary='yes'/>
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>
</video>
<!-- filesystems -->
<filesystem type='mount' accessmode='passthrough'>
<source dir='/nix/store'/>
<target dir='store'/>
<readonly/>
</filesystem>
<filesystem type='mount' accessmode='mapped'>
<source dir='%s'/>
<target dir='xchg'/> <!-- workaround for nixpkgs/nixos/modules/virtualisation/qemu-vm.nix -->
</filesystem>
<filesystem type='mount' accessmode='mapped'>
<source dir='%s'/>
<target dir='shared'/> <!-- workaround for nixpkgs/nixos/modules/virtualisation/qemu-vm.nix -->
</filesystem>
<filesystem type='mount' accessmode='mapped'>
<source dir='%s'/>
<target dir='home'/>
</filesystem>
</devices>
<qemu:commandline>
<qemu:arg value='-device'/>
<qemu:arg value='e1000,netdev=net0'/>
<qemu:arg value='-netdev'/>
<qemu:arg value='user,id=net0'/>
<qemu:arg value='-snapshot'/>
</qemu:commandline>
</domain>
`
func evalNix(expr string) (s string) {
command := exec.Command("nix", "eval", "--raw", expr)
bytes, _ := command.Output()
s = string(bytes)
return
}
// Gets an expression returning AppVM config path
func getAppVMExpressionPath(name string) string {
paths := strings.Split(os.Getenv("APPVM_CONFIGS"), ":")
for _, a := range paths {
searchpath := a + "/nix"
log.Print("Searching " + searchpath + " for expressions")
if _, err := os.Stat(searchpath); os.IsExist(err) {
exprpath := searchpath + "/" + name + ".nix"
if os.Stat(exprpath); os.IsExist(err) {
return exprpath
}
}
log.Print("Local repo " + searchpath + " doesn't have a nix expression for " + name)
}
log.Print("Trying to use remote repo config")
fetchFormat := "(builtins.fetchurl \"raw.githubusercontent.com/%[1]s/%[2]s/master/nix/%[3]s.nix\" )"
splitString := strings.Split(name, "/")
if len(splitString) != 3 {
// nope, not a repo format
return evalNix(fmt.Sprintf(fetchFormat, "jollheef", "appvm", name))
}
return evalNix(fmt.Sprintf(fetchFormat, splitString[0], splitString[1], splitString[2]))
}
2018-07-11 23:34:36 +00:00
func generateXML(name, vmNixPath, reginfo, img, sharedDir string) string {
// TODO: Define XML in go
return fmt.Sprintf(xmlTmpl, "appvm_"+name, vmNixPath, vmNixPath, vmNixPath,
reginfo, img, sharedDir, sharedDir, sharedDir)
}
func list(l *libvirt.Libvirt) {
domains, err := l.Domains()
if err != nil {
log.Fatal(err)
}
2018-07-12 00:06:43 +00:00
fmt.Println("Started VM:")
2018-07-11 23:34:36 +00:00
for _, d := range domains {
2019-05-12 00:11:09 +00:00
if strings.HasPrefix(d.Name, "appvm") {
2018-07-12 00:06:43 +00:00
fmt.Println("\t", d.Name[6:])
}
}
fmt.Println("\nAvailable VM:")
2019-02-03 22:22:47 +00:00
files, err := ioutil.ReadDir(os.Getenv("GOPATH") + "/src/code.dumpstack.io/tools/appvm/nix")
2018-07-12 00:06:43 +00:00
if err != nil {
log.Fatal(err)
}
for _, f := range files {
if f.Name() != "base.nix" &&
f.Name() != "local.nix" &&
f.Name() != "local.nix.template" {
2018-07-12 00:06:43 +00:00
fmt.Println("\t", f.Name()[0:len(f.Name())-4])
2018-07-11 23:34:36 +00:00
}
}
}
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()
}
2018-07-15 19:00:08 +00:00
func prepareTemplates(appvmPath string) (err error) {
if _, err = os.Stat(appvmPath + "/nix/local.nix"); os.IsNotExist(err) {
err = copyFile(appvmPath+"/nix/local.nix.template", appvmPath+"/nix/local.nix")
if err != nil {
return
}
}
return
}
2018-08-04 09:27:53 +00:00
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(name string, verbose bool) (realpath, reginfo, qcow2 string, err error) {
vmConfigPath := getAppVMExpressionPath(name)
log.Print(vmConfigPath)
2018-08-04 09:27:53 +00:00
command := cmd.NewCmdOptions(cmd.Options{Buffered: false, Streaming: true},
"nix-build", "<nixpkgs/nixos>", "-A", "config.system.build.vm",
"-I", "nixos-config="+vmConfigPath, "-I", ".")
2018-08-04 09:27:53 +00:00
if verbose {
go streamStdOutErr(command)
}
status := <-command.Start()
if status.Error != nil || status.Exit != 0 {
log.Println(status.Error, status.Stdout, status.Stderr)
2019-05-12 00:10:25 +00:00
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)
}
2018-07-15 19:08:28 +00:00
return
2018-07-11 23:34:36 +00:00
}
2018-07-15 19:08:28 +00:00
realpath, err = filepath.EvalSymlinks("result/system")
2018-07-11 23:34:36 +00:00
if err != nil {
2018-07-15 19:08:28 +00:00
return
2018-07-11 23:34:36 +00:00
}
2019-12-29 07:24:59 +00:00
bytes, err := ioutil.ReadFile("result/bin/run-nixos-vm")
2018-07-11 23:34:36 +00:00
if err != nil {
2018-07-15 19:08:28 +00:00
return
2018-07-11 23:34:36 +00:00
}
2019-12-29 07:24:59 +00:00
match := regexp.MustCompile("regInfo=.*/registration").FindSubmatch(bytes)
if len(match) != 1 {
err = errors.New("should be one reginfo")
return
}
reginfo = string(match[0])
2018-07-11 23:34:36 +00:00
syscall.Unlink("result")
qcow2 = os.Getenv("HOME") + "/appvm/.fake.qcow2"
2018-07-15 19:08:28 +00:00
if _, err = os.Stat(qcow2); os.IsNotExist(err) {
2018-07-11 23:34:36 +00:00
system.System("qemu-img", "create", "-f", "qcow2", qcow2, "512M")
2018-07-15 19:08:28 +00:00
err = os.Chmod(qcow2, 0400) // qemu run with -snapshot, we only need it for create /dev/vda
2018-07-11 23:34:36 +00:00
if err != nil {
2018-07-15 19:08:28 +00:00
return
2018-07-11 23:34:36 +00:00
}
}
2018-07-15 19:08:28 +00:00
return
}
2018-07-15 19:32:28 +00:00
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
}
2018-07-15 19:08:28 +00:00
2018-08-04 09:27:53 +00:00
func generateAppVM(l *libvirt.Libvirt, appvmPath, name string, verbose bool) (err error) {
2018-07-15 19:32:28 +00:00
err = os.Chdir(appvmPath)
2018-07-15 19:08:28 +00:00
if err != nil {
2018-07-15 19:32:28 +00:00
return
2018-07-15 19:08:28 +00:00
}
2018-07-11 23:34:36 +00:00
2018-08-04 09:27:53 +00:00
realpath, reginfo, qcow2, err := generateVM(name, verbose)
2018-07-11 23:34:36 +00:00
if err != nil {
2018-07-15 19:32:28 +00:00
return
2018-07-11 23:34:36 +00:00
}
2018-07-15 19:08:28 +00:00
sharedDir := fmt.Sprintf(os.Getenv("HOME") + "/appvm/" + name)
os.MkdirAll(sharedDir, 0700)
2018-07-11 23:34:36 +00:00
xml := generateXML(name, realpath, reginfo, qcow2, sharedDir)
_, err = l.DomainCreateXML(xml, libvirt.DomainStartValidate)
2018-07-15 19:32:28 +00:00
return
}
2018-07-15 19:54:38 +00:00
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("+")
}
}
}
2018-08-04 09:27:53 +00:00
func start(l *libvirt.Libvirt, name string, verbose bool) {
2018-07-15 19:32:28 +00:00
// Currently binary-only installation is not supported, because we need *.nix configurations
2019-02-03 22:22:47 +00:00
appvmPath := os.Getenv("GOPATH") + "/src/code.dumpstack.io/tools/appvm"
2018-07-15 19:32:28 +00:00
// Copy templates
err := prepareTemplates(appvmPath)
2018-07-11 23:34:36 +00:00
if err != nil {
log.Fatal(err)
}
2018-07-15 19:32:28 +00:00
if !isRunning(l, name) {
2018-08-04 09:27:53 +00:00
if !verbose {
go stupidProgressBar()
}
err = generateAppVM(l, appvmPath, name, verbose)
2018-07-15 19:32:28 +00:00
if err != nil {
log.Fatal(err)
}
}
2018-07-12 08:11:16 +00:00
cmd := exec.Command("virt-viewer", "appvm_"+name)
2018-07-11 23:34:36 +00:00
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)
}
}
2018-07-11 23:49:10 +00:00
err = l.DomainShutdown(dom)
2018-07-11 23:34:36 +00:00
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) {
2018-07-12 20:00:57 +00:00
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 d.Name[0:5] == "appvm" {
name := d.Name[6:]
memoryUsedRaw, err := ioutil.ReadFile(os.Getenv("HOME") + "/appvm/" + name + "/.memory_used")
if err != nil {
log.Fatal(err)
}
memoryUsedMiB, err := strconv.Atoi(string(memoryUsedRaw[0 : len(memoryUsedRaw)-1]))
if err != nil {
log.Fatal(err)
}
memoryUsed := memoryUsedMiB * 1024
_, memoryMax, memoryCurrent, _, _, err := l.DomainGetInfo(d)
if err != nil {
log.Fatal(err)
}
memoryNew := uint64(float64(memoryUsed) * (1 + float64(adjustPercent)/100))
2018-07-12 20:00:57 +00:00
if memoryNew > memoryMax {
memoryNew = memoryMax - 1
}
if memoryNew < memoryMin {
memoryNew = memoryMin
}
err = l.DomainSetMemory(d, memoryNew)
if err != nil {
log.Fatal(err)
}
table.Append([]string{name,
fmt.Sprintf("%d", memoryUsed),
fmt.Sprintf("%d", memoryCurrent),
fmt.Sprintf("%d", memoryMax),
fmt.Sprintf("%d", memoryNew)})
}
}
table.Render()
}
2018-07-11 23:34:36 +00:00
func main() {
2019-12-29 07:06:59 +00:00
os.Mkdir(os.Getenv("HOME")+"/appvm", 0700)
2018-07-11 23:34:36 +00:00
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()
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()
2018-08-10 07:16:58 +00:00
adjustPercent := autoballonCommand.Flag("adj-memory", "Adjust memory amount (percents)").Default("20").Uint64()
2018-08-04 09:27:53 +00:00
startCommand := kingpin.Command("start", "Start application")
startName := startCommand.Arg("name", "Application name").Required().String()
startVerbose := startCommand.Flag("verbose", "Increase verbosity").Default("False").Bool()
2018-07-11 23:34:36 +00:00
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()
switch kingpin.Parse() {
case "list":
list(l)
case "start":
2018-08-04 09:27:53 +00:00
start(l, *startName, *startVerbose)
2018-07-11 23:34:36 +00:00
case "stop":
stop(l, *stopName)
case "drop":
drop(*dropName)
2018-07-12 20:00:57 +00:00
case "autoballoon":
autoBalloon(l, *minMemory*1024, *adjustPercent)
2018-07-11 23:34:36 +00:00
}
}