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 (
|
|
|
|
"fmt"
|
2018-07-12 07:30:16 +00:00
|
|
|
"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"
|
2018-07-12 20:00:57 +00:00
|
|
|
"strconv"
|
2018-07-11 23:34:36 +00:00
|
|
|
"syscall"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/digitalocean/go-libvirt"
|
|
|
|
"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 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 {
|
|
|
|
if d.Name[0:5] == "appvm" {
|
2018-07-12 00:06:43 +00:00
|
|
|
fmt.Println("\t", d.Name[6:])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Println("\nAvailable VM:")
|
|
|
|
files, err := ioutil.ReadDir(os.Getenv("GOPATH") + "/src/github.com/jollheef/appvm/nix")
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, f := range files {
|
2018-07-12 07:34:18 +00:00
|
|
|
if f.Name() != "base.nix" &&
|
|
|
|
f.Name() != "local.nix" && f.Name() != "monitor.nix" &&
|
|
|
|
f.Name() != "local.nix.template" && f.Name() != "monitor.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
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-12 07:34:18 +00:00
|
|
|
func copyFile(from, to string) (err error) {
|
2018-07-12 07:30:16 +00:00
|
|
|
source, err := os.Open(from)
|
|
|
|
if err != nil {
|
2018-07-12 07:34:18 +00:00
|
|
|
return
|
2018-07-12 07:30:16 +00:00
|
|
|
}
|
|
|
|
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-11 23:34:36 +00:00
|
|
|
func start(l *libvirt.Libvirt, name string) {
|
|
|
|
// Currently binary-only installation is not supported, because we need *.nix configurations
|
|
|
|
gopath := os.Getenv("GOPATH")
|
2018-07-12 07:30:16 +00:00
|
|
|
appvmPath := gopath + "/src/github.com/jollheef/appvm"
|
|
|
|
err := os.Chdir(appvmPath)
|
2018-07-11 23:34:36 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2018-07-12 07:30:16 +00:00
|
|
|
// Copy templates
|
|
|
|
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 {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := os.Stat(appvmPath + "/nix/monitor.nix"); os.IsNotExist(err) {
|
|
|
|
err = copyFile(appvmPath+"/nix/monitor.nix.template", appvmPath+"/nix/monitor.nix")
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-12 00:00:58 +00:00
|
|
|
stdout, stderr, ret, err := system.System("nix-build", "<nixpkgs/nixos>", "-A", "config.system.build.vm",
|
2018-07-11 23:34:36 +00:00
|
|
|
"-I", "nixos-config=nix/"+name+".nix", "-I", ".")
|
|
|
|
if err != nil {
|
2018-07-12 00:00:58 +00:00
|
|
|
log.Fatalln(err, stdout, stderr, ret)
|
2018-07-11 23:34:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
realpath, err := filepath.EvalSymlinks("result/system")
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Use go regex
|
|
|
|
reginfo, _, _, err := system.System("sh", "-c", "cat result/bin/run-nixos-vm | grep -o 'regInfo=.*/registration'")
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
syscall.Unlink("result")
|
|
|
|
|
|
|
|
qcow2 := "/tmp/.appvm.fake.qcow2"
|
|
|
|
if _, err := os.Stat(qcow2); os.IsNotExist(err) {
|
|
|
|
system.System("qemu-img", "create", "-f", "qcow2", qcow2, "512M")
|
|
|
|
err := os.Chmod(qcow2, 0400) // qemu run with -snapshot, we only need it for create /dev/vda
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sharedDir := fmt.Sprintf(os.Getenv("HOME") + "/appvm/" + name)
|
|
|
|
os.MkdirAll(sharedDir, 0700)
|
|
|
|
|
|
|
|
// TODO: Search go libraries for manipulate ACL
|
|
|
|
_, _, _, err = system.System("setfacl", "-R", "-m", "u:qemu:rwx", os.Getenv("HOME")+"/appvm/")
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
xml := generateXML(name, realpath, reginfo, qcow2, sharedDir)
|
|
|
|
_, err = l.DomainCreateXML(xml, libvirt.DomainStartValidate)
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2018-07-12 20:00:57 +00:00
|
|
|
func autoBalloon(l *libvirt.Libvirt) {
|
|
|
|
domains, err := l.Domains()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
const memoryMin = 512 * 1024 // 512 MiB
|
|
|
|
|
|
|
|
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.2) // +20%
|
|
|
|
|
|
|
|
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() {
|
|
|
|
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")
|
2018-07-12 20:00:57 +00:00
|
|
|
kingpin.Command("autoballoon", "Automatically adjust/reduce app vm memory")
|
2018-07-11 23:34:36 +00:00
|
|
|
startName := kingpin.Command("start", "Start application").Arg("name", "Application name").Required().String()
|
|
|
|
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":
|
|
|
|
start(l, *startName)
|
|
|
|
case "stop":
|
|
|
|
stop(l, *stopName)
|
|
|
|
case "drop":
|
|
|
|
drop(*dropName)
|
2018-07-12 20:00:57 +00:00
|
|
|
case "autoballoon":
|
|
|
|
autoBalloon(l)
|
2018-07-11 23:34:36 +00:00
|
|
|
}
|
|
|
|
}
|