Rewrite in go, use libvirt

This commit is contained in:
dump_stack() 2018-07-11 23:34:36 +00:00
parent fbf4fa0e5a
commit eef6ed2ec0
12 changed files with 258 additions and 111 deletions

9
.gitignore vendored
View File

@ -1,9 +0,0 @@
bin/*
!bin/.keep
qemu/qcow2/*
!qemu/qcow2/.keep
qemu/bin/*
!qemu/bin/.keep
share/*
!share/.keep
nix/local.nix

View File

@ -4,9 +4,7 @@ Simple application VM's based on Nix package manager.
Uses one **read-only** /nix directory for all appvms. So creating a new appvm (but not first) is just about one minute. Uses one **read-only** /nix directory for all appvms. So creating a new appvm (but not first) is just about one minute.
Designed primarily for full screen usage (but remote-viewer has ability to resize window dynamically without change resolution) without guest additions (because of **less attack surface**). Currently optimized for full screen usage (but remote-viewer has ability to resize window dynamically without change resolution) without guest additions.
It's a proof-of-concept, but you can still use it. Also there is a lot of strange things inside, don't afraid of :)
![appvm screenshot](screenshots/2018-07-05.png) ![appvm screenshot](screenshots/2018-07-05.png)
@ -19,41 +17,41 @@ It's a proof-of-concept, but you can still use it. Also there is a lot of strang
$ su -c 'USE="spice virtfs" emerge qemu virt-manager' $ su -c 'USE="spice virtfs" emerge qemu virt-manager'
## Add appvm to PATH ## Libvirt from user (required if you need access to shared files)
$ echo 'PATH=$PATH:$HOME/appvm/bin' >> ~/.bashrc $ echo user = "$USER" | sudo tee -a /etc/libvirt/qemu.conf
(if you clone appvm to home directory) ## Install appvm tool
$ go get github.com/jollheef/appvm
## Generate resolution ## Generate resolution
By default uses 3840x2160. If you need to regenerate `appvm/nix/monitor.nix`: By default uses 3840x2160. If you need to regenerate `appvm/nix/monitor.nix`:
$ appvm/appvm.sh generate-resolution 1920 1080 > appvm/nix/monitor.nix $ $GOPATH/github.com/jollheef/appvm/generate-resolution.sh 1920 1080 > $GOPATH/github.com/jollheef/appvm/nix/monitor.nix
Autodetection is a bash-spaghetti, so you need to check results. BTW it's just a X.org monitor section. Autodetection is a bash-spaghetti, so you need to check results. BTW it's just a X.org monitor section.
## Create VM
$ $HOME/appvm/appvm.sh build chromium
You can customize local settings in `nix/local.nix`.
## Run application ## Run application
$ appvm.chromium ($GOPATH/bin must be in $PATH)
$ appvm start chromium
You can customize local settings in `$GOPATH/github.com/jollheef/appvm/nix/local.nix`.
Default hotkey to release cursor: ctrl+alt. Default hotkey to release cursor: ctrl+alt.
## Shared directory ## Shared directory
$ ls appvm/share/chromium $ ls appvm/chromium
foo.tar.gz foo.tar.gz
bar.tar.gz bar.tar.gz
## Close VM ## Close VM
$ pkill.... :) $ appvm stop chromium
# App description # App description

212
appvm.go Normal file
View File

@ -0,0 +1,212 @@
/**
* @author Mikhail Klementev jollheef<AT>riseup.net
* @license GNU GPLv3
* @date July 2018
* @brief appvm launcher
*/
package main
import (
"fmt"
"log"
"net"
"os"
"os/exec"
"path/filepath"
"syscall"
"time"
"github.com/digitalocean/go-libvirt"
"github.com/jollheef/go-system"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
var xmlTmpl = `
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
<name>%s</name>
<memory unit='KiB'>1048576</memory>
<currentMemory unit='KiB'>1048576</currentMemory>
<vcpu placement='static'>1</vcpu>
<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>
<!-- 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)
}
// TODO list available to create VM's too
for _, d := range domains {
if d.Name[0:5] == "appvm" {
fmt.Println(d.Name[6:])
}
}
}
func start(l *libvirt.Libvirt, name string) {
// Currently binary-only installation is not supported, because we need *.nix configurations
gopath := os.Getenv("GOPATH")
err := os.Chdir(gopath + "/src/github.com/jollheef/appvm")
if err != nil {
log.Fatal(err)
}
_, _, _, err = system.System("nix-build", "<nixpkgs/nixos>", "-A", "config.system.build.vm",
"-I", "nixos-config=nix/"+name+".nix", "-I", ".")
if err != nil {
log.Fatal(err)
}
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)
}
cmd := exec.Command("virt-viewer", "-f", "appvm_"+name)
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.DomainDestroy(dom)
if err != nil {
log.Fatal(err)
}
}
func drop(name string) {
appDataPath := fmt.Sprintf(os.Getenv("HOME") + "/appvm/" + name)
os.RemoveAll(appDataPath)
}
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")
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)
}
}

View File

@ -1,49 +0,0 @@
#!/bin/bash
APPVM_PATH=$(dirname $(realpath $0))
cd ${APPVM_PATH}
if [ ! -f nix/local.nix ]; then
echo "[*] There is no local.nix, creating."
echo -e "{\n}" >> nix/local.nix
fi
if [[ "$1" == "build" && "$2" != "" ]]; then
if [ -f bin/appvm.${2} ]; then
echo "[*] Kill app."
pkill -f "$(cat bin/appvm.${2} | grep pgrep | awk '{ print $3 }')"
fi
if [ -f qemu/qcow2/${2}.qcow2 ]; then
echo "[*] Remove old app state."
rm qemu/qcow2/${2}.qcow2
fi
NIX_PATH=$NIX_PATH:. nix-build '<nixpkgs/nixos>' -A config.system.build.vm -I nixos-config=nix/${2}.nix || exit 1
NIX_SYSTEM=$(realpath result/system)
mkdir -p bin
RAND_HASH=$(head /dev/urandom | md5sum | awk '{ print $1 }')
VM_BIN_PATH=$(realpath qemu/bin/qemu.${RAND_HASH}.${2})
sed "s;NIX_SYSTEM_PLACEHOLDER;${NIX_SYSTEM};" qemu/qemu.template > ${VM_BIN_PATH}
sed -i "s;NAME_PLACEHOLDER;${2};" ${VM_BIN_PATH}
sed -i "s;HASH_PLACEHOLDER;${RAND_HASH};" ${VM_BIN_PATH}
sed -i "s;NIX_DISK_IMAGE_PLACEHOLDER;${APPVM_PATH}/qemu/qcow2/${2}.qcow2;" ${VM_BIN_PATH}
RANDOM_PORT=$(/usr/bin/python -c 'import random; print(random.randint(1024,65535))')
# TODO Check for port collisions
sed -i "s;PORT_PLACEHOLDER;${RANDOM_PORT};" ${VM_BIN_PATH}
echo -e "#!/bin/bash\npgrep -f ${RAND_HASH} || {\n\tnohup setsid ${VM_BIN_PATH} >/dev/null 2>&1 &\n\tsleep 1s\n}\nremote-viewer -f spice://127.200.0.1:${RANDOM_PORT}" > bin/appvm.${2}
chmod +x ${VM_BIN_PATH}
chmod +x bin/appvm.${2}
unlink result
elif [[ "$1" == "generate-resolution" && "$2" != "" && "$3" != "" ]]; then
MONITOR_SIZE="$(xrandr | grep mm | head -n 1 | awk '{ print $(NF-2) " " $(NF) }' | sed 's/mm//g')"
CVT="$(cvt ${2} ${3} | grep Modeline)"
echo "{"
echo " services.xserver.monitorSection = ''"
echo " " ${CVT}
echo " " Option '"PreferredMode"' $(echo ${CVT} | awk '{ print $2 }')
echo " " DisplaySize ${MONITOR_SIZE} # In millimeters
echo " '';"
echo "}"
else
echo -e "Usage:\t$0 build APPLICATION"
echo -e "or:\t$0 generate-resolution X Y"
fi

View File

16
generate-resolution.sh Executable file
View File

@ -0,0 +1,16 @@
#!/bin/sh
if [[ "$1" == "" || "$2" == "" ]]; then
echo -e "Usage:\t$0 X Y"
exit 1
fi
MONITOR_SIZE="$(xrandr | grep mm | head -n 1 | awk '{ print $(NF-2) " " $(NF) }' | sed 's/mm//g')"
CVT="$(cvt ${1} ${2} | grep Modeline)"
echo "{"
echo " services.xserver.monitorSection = ''"
echo " " ${CVT}
echo " " Option '"PreferredMode"' $(echo ${CVT} | awk '{ print $2 }')
echo " " DisplaySize ${MONITOR_SIZE} # In millimeters
echo " '';"
echo "}"

View File

@ -36,11 +36,22 @@ main = xmonad defaultConfig
description = "Create and xmonad configuration"; description = "Create and xmonad configuration";
serviceConfig = { serviceConfig = {
ConditionFileNotEmpty = "!/home/user/.xmonad/xmonad.hs"; ConditionFileNotEmpty = "!/home/user/.xmonad/xmonad.hs";
ExecStart = "/bin/sh -c 'mkdir /home/user/.xmonad && cp /etc/xmonad.hs /home/user/.xmonad/xmonad.hs'"; ExecStart = "/bin/sh -c 'mkdir -p /home/user/.xmonad && cp /etc/xmonad.hs /home/user/.xmonad/xmonad.hs'";
RemainAfterExit = "yes"; RemainAfterExit = "yes";
Type = "oneshot"; Type = "oneshot";
User = "user"; User = "user";
}; };
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
}; };
systemd.services.mount-home-user = {
description = "Mount /home/user (crutch)";
serviceConfig = {
ExecStart = "/bin/sh -c '/run/current-system/sw/bin/mount -t 9p -o trans=virtio,version=9p2000.L,uid=1000 home /home/user'";
RemainAfterExit = "yes";
Type = "oneshot";
User = "root";
};
wantedBy = [ "sysinit.target" ];
};
} }

4
nix/local.nix Normal file
View File

@ -0,0 +1,4 @@
{
services.xserver.layout = "us,ru";
services.xserver.xkbOptions = "ctrl:nocaps,grp:rctrl_toggle";
}

View File

View File

View File

@ -1,36 +0,0 @@
#!/bin/bash
NAME=NAME_PLACEHOLDER
NIX_DISK_IMAGE=NIX_DISK_IMAGE_PLACEHOLDER
if ! test -e "$NIX_DISK_IMAGE"; then
qemu-img create -f qcow2 "$NIX_DISK_IMAGE" 512M || exit 1
fi
# Create a directory for storing temporary data of the running VM.
TMPDIR=$(dirname ${NIX_DISK_IMAGE})/../../share/NAME_PLACEHOLDER
# Create a directory for exchanging data with the VM.
mkdir -p $TMPDIR
cd $TMPDIR
NIX_SYSTEM="NIX_SYSTEM_PLACEHOLDER"
# Start QEMU.
qemu-system-x86_64 -enable-kvm \
-name NAME_PLACEHOLDER_HASH_PLACEHOLDER \
-m 1024 \
-smp 1 \
-device virtio-rng-pci \
-net nic,netdev=user.0,model=virtio -netdev user,id=user.0${QEMU_NET_OPTS:+,$QEMU_NET_OPTS} \
-spice port=PORT_PLACEHOLDER,addr=127.200.0.1,disable-ticketing,image-compression=off,seamless-migration=on \
-sandbox on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny \
-virtfs local,path=/nix/store,security_model=none,mount_tag=store,readonly \
-virtfs local,path=$TMPDIR,security_model=none,mount_tag=xchg \
-virtfs local,path=${SHARED_DIR:-$TMPDIR},security_model=none,mount_tag=shared \
-drive index=0,id=drive$((0 + 1)),file=$NIX_DISK_IMAGE,cache=writeback,werror=report,if=virtio \
-kernel ${NIX_SYSTEM}/kernel \
-initrd ${NIX_SYSTEM}/initrd \
-append "$(cat ${NIX_SYSTEM}/kernel-params) init=${NIX_SYSTEM}/init regInfo=/nix/store/622pn30mg7z4knkrqsh3acrjyaiyq6sr-closure-info/registration" \
-device qxl-vga,vgamem_mb=256 #-display gtk

View File