mirror of
https://code.dumpstack.io/tools/appvm.git
synced 2024-11-27 10:31:31 +00:00
Rewrite in go, use libvirt
This commit is contained in:
parent
fbf4fa0e5a
commit
eef6ed2ec0
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1,9 +0,0 @@
|
|||
bin/*
|
||||
!bin/.keep
|
||||
qemu/qcow2/*
|
||||
!qemu/qcow2/.keep
|
||||
qemu/bin/*
|
||||
!qemu/bin/.keep
|
||||
share/*
|
||||
!share/.keep
|
||||
nix/local.nix
|
30
README.md
30
README.md
|
@ -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.
|
||||
|
||||
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**).
|
||||
|
||||
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 :)
|
||||
Currently optimized for full screen usage (but remote-viewer has ability to resize window dynamically without change resolution) without guest additions.
|
||||
|
||||
![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'
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
|
||||
## Create VM
|
||||
|
||||
$ $HOME/appvm/appvm.sh build chromium
|
||||
|
||||
You can customize local settings in `nix/local.nix`.
|
||||
|
||||
## 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.
|
||||
|
||||
## Shared directory
|
||||
|
||||
$ ls appvm/share/chromium
|
||||
$ ls appvm/chromium
|
||||
foo.tar.gz
|
||||
bar.tar.gz
|
||||
|
||||
## Close VM
|
||||
|
||||
$ pkill.... :)
|
||||
$ appvm stop chromium
|
||||
|
||||
# App description
|
||||
|
||||
|
|
212
appvm.go
Normal file
212
appvm.go
Normal 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)
|
||||
}
|
||||
}
|
49
appvm.sh
49
appvm.sh
|
@ -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
|
16
generate-resolution.sh
Executable file
16
generate-resolution.sh
Executable 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 "}"
|
13
nix/base.nix
13
nix/base.nix
|
@ -36,11 +36,22 @@ main = xmonad defaultConfig
|
|||
description = "Create and xmonad configuration";
|
||||
serviceConfig = {
|
||||
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";
|
||||
Type = "oneshot";
|
||||
User = "user";
|
||||
};
|
||||
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
4
nix/local.nix
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
services.xserver.layout = "us,ru";
|
||||
services.xserver.xkbOptions = "ctrl:nocaps,grp:rctrl_toggle";
|
||||
}
|
|
@ -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
|
Loading…
Reference in a new issue