Compare commits
158 Commits
v0.1
...
ef856f9e1f
Author | SHA1 | Date | |
---|---|---|---|
ef856f9e1f
|
|||
902ce18661
|
|||
2e87dbf424
|
|||
921da44d09
|
|||
9a410f75d8
|
|||
7b09716347
|
|||
6ac189e1e8
|
|||
4ac4969b66
|
|||
17f17be784 | |||
467f74f19a
|
|||
ce92ddf1b9
|
|||
d698f058ff
|
|||
84374314a2
|
|||
a3bd38c243
|
|||
5753321877 | |||
c6159a7925 | |||
2560e852df | |||
f063f2a90c | |||
33facb2321
|
|||
9142fe53c6
|
|||
c2413d0208 | |||
d9c651987b | |||
3483763938 | |||
bfc28be996 | |||
6321004848 | |||
5376e7a56f | |||
77eada72c3 | |||
9a602a2231 | |||
e6f086f88a
|
|||
8264f71eb2
|
|||
ba80fba648 | |||
e588479701 | |||
a583335865 | |||
aaede60c4d
|
|||
6e26754cec
|
|||
c0801541b3 | |||
63cd3fc79d
|
|||
00f705d757
|
|||
218e81a6ac
|
|||
7cad1efce8
|
|||
cc5262b10f
|
|||
e02edbacf4
|
|||
2bc5247ac1
|
|||
545291b9cf
|
|||
9b828ff575
|
|||
dc636c3919
|
|||
d07d51c7ec
|
|||
19b92aec1b
|
|||
680aff06f7
|
|||
7b76368699
|
|||
5a2ce12c2f
|
|||
f8bfca0466
|
|||
01a0d206db
|
|||
44cb4697ec
|
|||
219c48b976
|
|||
c68f3bf10d
|
|||
8b271a3e5e
|
|||
2146397906
|
|||
1fbbdee76a
|
|||
1f4214199e
|
|||
4e7c6e9895
|
|||
88718323f0
|
|||
4ce1c5643f
|
|||
9c9a71c2c8
|
|||
fca2f89046
|
|||
bd63fcd48f
|
|||
f15d0fb07a
|
|||
a264cb2ced
|
|||
33cdae7a47
|
|||
41f25418e8
|
|||
2be0f82e4a
|
|||
dad147eb91
|
|||
806448df17
|
|||
573be66059
|
|||
7df36ae8fc
|
|||
4066e347ef
|
|||
de77abd3ee
|
|||
cead5bab74
|
|||
6536200669
|
|||
1095218222
|
|||
a4138a4b44
|
|||
287c6d6043
|
|||
477b48d783
|
|||
843ca6864e
|
|||
02832a6e38
|
|||
3f8aa0cc59
|
|||
5526cbee02
|
|||
7d6d524b67
|
|||
40ef3fe50e
|
|||
d973179557
|
|||
49b8790032
|
|||
355fb314a1
|
|||
e037770c38
|
|||
57e15fa0a0
|
|||
f1fd2e1505
|
|||
02dda8bcf9
|
|||
13226a6a79
|
|||
0c35a66606
|
|||
dbfc2929db
|
|||
3e8a08d638
|
|||
8baa1ff73b
|
|||
25a2f45e59
|
|||
af5691b0d4
|
|||
6c72b5de00
|
|||
04b2cf63ce
|
|||
7e4aa33a0a
|
|||
bc704df503
|
|||
43d42c2242
|
|||
f4c6a6a90b
|
|||
0a51db3bde
|
|||
99bd71b80c
|
|||
3416808444
|
|||
9c6b8a0122
|
|||
cc92ec3e23
|
|||
6ebc562599
|
|||
f18d55bd27
|
|||
197a78f595 | |||
555768d03b | |||
4104d91eab | |||
2d5b72219f
|
|||
84095193e5
|
|||
718ab3a164
|
|||
6b35acf673
|
|||
38a3ba301b
|
|||
29e447435f
|
|||
8a93015bfe
|
|||
e7cc81694b
|
|||
6505544535
|
|||
24927bc787
|
|||
1251a21aba
|
|||
e5ccd6d913
|
|||
0823b2f028 | |||
4ce9b6ee86 | |||
ba569d4aab | |||
302d004e85 | |||
55a825a7c0
|
|||
1270b2e209
|
|||
870c9d6da8
|
|||
bd8d6d822d
|
|||
23947390a1
|
|||
6a54439180
|
|||
959e6e6596
|
|||
8bcf3eae87
|
|||
bb5a352215
|
|||
660c8a5dba
|
|||
9ac5d98ddb
|
|||
20b9b42e03 | |||
fd224db22e
|
|||
f60945d3c1
|
|||
575e147989
|
|||
ff4e7f8398
|
|||
9218a10641
|
|||
326b9ce610 | |||
36267ac5ea | |||
5bb305a0ec | |||
5f81e7ac3d | |||
cb343c4506 | |||
02f4eab277 |
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<!-- Makes sure these boxes are checked before submitting your pull request -- thank you! -->
|
||||||
|
|
||||||
|
- [ ] I tested it locally.
|
||||||
|
- [ ] I tried to run at least one application VM and it works.
|
33
.github/workflows/ubuntu.yml
vendored
Normal file
33
.github/workflows/ubuntu.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
name: Ubuntu
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
|
- name: Fetch dependencies
|
||||||
|
run: go get -d ./...
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build
|
||||||
|
|
||||||
|
nix-build:
|
||||||
|
name: nix-build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
run: |
|
||||||
|
curl -L https://nixos.org/nix/install | sh
|
||||||
|
. ~/.nix-profile/etc/profile.d/nix.sh
|
||||||
|
nix-channel --update
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
. ~/.nix-profile/etc/profile.d/nix.sh
|
||||||
|
make nix
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1 @@
|
|||||||
nix/local.nix
|
appvm
|
||||||
nix/monitor.nix
|
|
||||||
|
5
Makefile
Normal file
5
Makefile
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
go:
|
||||||
|
go build
|
||||||
|
|
||||||
|
nix:
|
||||||
|
nix-build #-E '((import <nixpkgs> {}).callPackage (import ./default.nix) { })' --option sandbox true --no-out-link
|
84
README.md
84
README.md
@ -1,72 +1,47 @@
|
|||||||
|
[](https://appvm.readthedocs.io/en/latest/?badge=latest)
|
||||||
|
|
||||||
# Nix application VMs: security through virtualization
|
# Nix application VMs: security through virtualization
|
||||||
|
|
||||||
Simple application VMs (hypervisor-based sandbox) based on Nix package manager.
|
Simple application VMs (hypervisor-based sandbox) 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.
|
||||||
|
|
||||||
Currently optimized for full screen usage (but remote-viewer has ability to resize window dynamically without change resolution).
|

|
||||||
|
|
||||||

|
## Installation
|
||||||
|
|
||||||
## Dependencies
|
See [related documentation](https://appvm.readthedocs.io/en/latest/installation.html).
|
||||||
|
|
||||||
$ sudo apt install golang virt-manager curl git
|
## Usage
|
||||||
$ sudo usermod -a -G libvirt $USER
|
|
||||||
|
|
||||||
$ echo 'export GOPATH=$HOME/go' >> ~/.bash_profile
|
### Search for applications
|
||||||
$ echo 'export PATH=$PATH:$GOPATH/bin' >> ~/.bash_profile
|
|
||||||
$ echo 'source ~/.bash_profile' >> ~/.bashrc
|
|
||||||
$ source ~/.bash_profile
|
|
||||||
|
|
||||||
You need to **relogin** if you install virt-manager (libvirt) first time.
|
$ appvm search chromium
|
||||||
|
|
||||||
## Install Nix package manager
|
### Run application
|
||||||
|
|
||||||
$ sudo mkdir -m 0755 /nix && sudo chown $USER /nix
|
|
||||||
$ curl https://nixos.org/nix/install | sh
|
|
||||||
$ . ~/.nix-profile/etc/profile.d/nix.sh
|
|
||||||
|
|
||||||
## Libvirt from user (required if you need access to shared files)
|
|
||||||
|
|
||||||
$ echo user = "\"$USER\"" | sudo tee -a /etc/libvirt/qemu.conf
|
|
||||||
$ sudo systemctl restart libvirtd
|
|
||||||
|
|
||||||
## Install appvm tool
|
|
||||||
|
|
||||||
$ go get github.com/jollheef/appvm
|
|
||||||
|
|
||||||
## Update appvm tool
|
|
||||||
|
|
||||||
$ go get -u github.com/jollheef/appvm
|
|
||||||
|
|
||||||
## Generate resolution
|
|
||||||
|
|
||||||
By default uses 1920x1080. If you need to regenerate `appvm/nix/monitor.nix`:
|
|
||||||
|
|
||||||
$ $GOPATH/src/github.com/jollheef/appvm/generate-resolution.sh 3840 2160 > $GOPATH/src/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.
|
|
||||||
|
|
||||||
## Run application
|
|
||||||
|
|
||||||
$ appvm start chromium
|
$ appvm start chromium
|
||||||
$ # ... long wait for first time, because we need to collect a lot of packages
|
$ # ... long wait for first time, because we need to collect a lot of packages
|
||||||
|
|
||||||
You can customize local settings in `$GOPATH/github.com/jollheef/appvm/nix/local.nix`.
|
### Synchronize remote repos for applications
|
||||||
|
|
||||||
|
$ appvm sync
|
||||||
|
|
||||||
|
You can customize local settings in **~/.config/appvm/nix/local.nix**.
|
||||||
|
|
||||||
Default hotkey to release cursor: ctrl+alt.
|
Default hotkey to release cursor: ctrl+alt.
|
||||||
|
|
||||||
## Shared directory
|
### Shared directory
|
||||||
|
|
||||||
$ ls appvm/chromium
|
$ ls appvm/chromium
|
||||||
foo.tar.gz
|
foo.tar.gz
|
||||||
bar.tar.gz
|
bar.tar.gz
|
||||||
|
|
||||||
## Close VM
|
### Close VM
|
||||||
|
|
||||||
$ appvm stop chromium
|
$ appvm stop chromium
|
||||||
|
|
||||||
## Automatic ballooning
|
### Automatic ballooning
|
||||||
|
|
||||||
Add this command:
|
Add this command:
|
||||||
|
|
||||||
@ -76,28 +51,3 @@ to crontab like that:
|
|||||||
|
|
||||||
$ crontab -l
|
$ crontab -l
|
||||||
* * * * * /home/user/dev/go/bin/appvm autoballoon
|
* * * * * /home/user/dev/go/bin/appvm autoballoon
|
||||||
|
|
||||||
# App description
|
|
||||||
|
|
||||||
$ cat nix/chromium.nix
|
|
||||||
{pkgs, ...}:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
<nixpkgs/nixos/modules/virtualisation/qemu-vm.nix>
|
|
||||||
<nix/base.nix>
|
|
||||||
];
|
|
||||||
|
|
||||||
environment.systemPackages = [ pkgs.chromium ];
|
|
||||||
services.xserver.displayManager.sessionCommands = "while [ 1 ]; do ${pkgs.chromium}/bin/chromium; done &";
|
|
||||||
}
|
|
||||||
|
|
||||||
For create new app you should add package name (search at https://nixos.org/nixos/packages.html) and path to binary (typically same as package name).
|
|
||||||
|
|
||||||
## Defined applications (pull requests are welcome!)
|
|
||||||
|
|
||||||
* chromium
|
|
||||||
* thunderbird
|
|
||||||
* tdesktop
|
|
||||||
* evince
|
|
||||||
* libreoffice
|
|
||||||
* wire
|
|
||||||
|
492
appvm.go
492
appvm.go
@ -8,97 +8,36 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/digitalocean/go-libvirt"
|
"github.com/digitalocean/go-libvirt"
|
||||||
|
"github.com/go-cmd/cmd"
|
||||||
"github.com/jollheef/go-system"
|
"github.com/jollheef/go-system"
|
||||||
"github.com/olekukonko/tablewriter"
|
"github.com/olekukonko/tablewriter"
|
||||||
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var xmlTmpl = `
|
type networkModel int
|
||||||
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
|
|
||||||
<name>%s</name>
|
|
||||||
<memory unit='GiB'>2</memory>
|
|
||||||
<currentMemory unit='GiB'>1</currentMemory>
|
|
||||||
<vcpu>4</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>
|
|
||||||
<!-- Guest additionals support -->
|
|
||||||
<channel type='spicevmc'>
|
|
||||||
<target type='virtio' name='com.redhat.spice.0'/>
|
|
||||||
</channel>
|
|
||||||
<!-- 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 {
|
const (
|
||||||
// TODO: Define XML in go
|
networkOffline networkModel = iota
|
||||||
return fmt.Sprintf(xmlTmpl, "appvm_"+name, vmNixPath, vmNixPath, vmNixPath,
|
networkQemu networkModel = iota
|
||||||
reginfo, img, sharedDir, sharedDir, sharedDir)
|
networkLibvirt networkModel = iota
|
||||||
}
|
)
|
||||||
|
|
||||||
func list(l *libvirt.Libvirt) {
|
func list(l *libvirt.Libvirt) {
|
||||||
domains, err := l.Domains()
|
domains, err := l.Domains()
|
||||||
@ -108,23 +47,27 @@ func list(l *libvirt.Libvirt) {
|
|||||||
|
|
||||||
fmt.Println("Started VM:")
|
fmt.Println("Started VM:")
|
||||||
for _, d := range domains {
|
for _, d := range domains {
|
||||||
if d.Name[0:5] == "appvm" {
|
if strings.HasPrefix(d.Name, "appvm") {
|
||||||
fmt.Println("\t", d.Name[6:])
|
fmt.Println("\t", d.Name[6:])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nAvailable VM:")
|
fmt.Println("\nAvailable VM:")
|
||||||
files, err := ioutil.ReadDir(os.Getenv("GOPATH") + "/src/github.com/jollheef/appvm/nix")
|
files, err := ioutil.ReadDir(configDir + "/nix")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
if f.Name() != "base.nix" &&
|
switch f.Name() {
|
||||||
f.Name() != "local.nix" && f.Name() != "monitor.nix" &&
|
case "base.nix":
|
||||||
f.Name() != "local.nix.template" && f.Name() != "monitor.nix.template" {
|
continue
|
||||||
fmt.Println("\t", f.Name()[0:len(f.Name())-4])
|
case "local.nix":
|
||||||
|
continue
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println("\t", f.Name()[0:len(f.Name())-4])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,75 +92,200 @@ func copyFile(from, to string) (err error) {
|
|||||||
return destination.Close()
|
return destination.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func start(l *libvirt.Libvirt, name string) {
|
func prepareTemplates(appvmPath string) (err error) {
|
||||||
// Currently binary-only installation is not supported, because we need *.nix configurations
|
if _, err = os.Stat(appvmPath + "/nix/local.nix"); os.IsNotExist(err) {
|
||||||
gopath := os.Getenv("GOPATH")
|
err = ioutil.WriteFile(configDir+"/nix/local.nix", local_nix_template, 0644)
|
||||||
appvmPath := gopath + "/src/github.com/jollheef/appvm"
|
|
||||||
err := os.Chdir(appvmPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
return
|
||||||
err = copyFile(appvmPath+"/nix/monitor.nix.template", appvmPath+"/nix/monitor.nix")
|
}
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
stdout, stderr, ret, err := system.System("nix-build", "<nixpkgs/nixos>", "-A", "config.system.build.vm",
|
status := <-command.Start()
|
||||||
"-I", "nixos-config=nix/"+name+".nix", "-I", ".")
|
if status.Error != nil || status.Exit != 0 {
|
||||||
if err != nil {
|
log.Println(status.Error, status.Stdout, status.Stderr)
|
||||||
log.Fatalln(err, stdout, stderr, ret)
|
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")
|
realpath, err = filepath.EvalSymlinks("result/system")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Use go regex
|
matches, err := filepath.Glob("result/bin/run-*-vm")
|
||||||
reginfo, _, _, err := system.System("sh", "-c", "cat result/bin/run-nixos-vm | grep -o 'regInfo=.*/registration'")
|
if err != nil || len(matches) != 1 {
|
||||||
if err != nil {
|
return
|
||||||
log.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
syscall.Unlink("result")
|
||||||
|
|
||||||
qcow2 := "/tmp/.appvm.fake.qcow2"
|
tmpraw := os.Getenv("HOME") + "/appvm/." + name + ".tmp.raw"
|
||||||
if _, err := os.Stat(qcow2); os.IsNotExist(err) {
|
qcow2 = os.Getenv("HOME") + "/appvm/." + name + ".fake.qcow2"
|
||||||
system.System("qemu-img", "create", "-f", "qcow2", qcow2, "512M")
|
if _, e := os.Stat(qcow2); os.IsNotExist(e) {
|
||||||
err := os.Chmod(qcow2, 0400) // qemu run with -snapshot, we only need it for create /dev/vda
|
system.System("qemu-img", "create", "-f", "raw", tmpraw, "40M")
|
||||||
if err != nil {
|
system.System("mkfs.ext4", "-L", "nixos", tmpraw)
|
||||||
log.Fatal(err)
|
system.System("qemu-img", "convert", "-f", "raw", "-O", "qcow2", tmpraw, qcow2)
|
||||||
}
|
system.System("rm", tmpraw)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
sharedDir := fmt.Sprintf(os.Getenv("HOME") + "/appvm/" + name)
|
|
||||||
os.MkdirAll(sharedDir, 0700)
|
os.MkdirAll(sharedDir, 0700)
|
||||||
|
|
||||||
// TODO: Search go libraries for manipulate ACL
|
vmName := "appvm_"
|
||||||
_, _, _, err = system.System("setfacl", "-R", "-m", "u:qemu:rwx", os.Getenv("HOME")+"/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 {
|
if err != nil {
|
||||||
log.Fatal(err)
|
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", "appvm_"+name)
|
if gui {
|
||||||
|
cmd := exec.Command("virt-viewer", "-c", "qemu:///system", vmName)
|
||||||
cmd.Start()
|
cmd.Start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop(l *libvirt.Libvirt, name string) {
|
func stop(l *libvirt.Libvirt, name string) {
|
||||||
@ -241,36 +309,41 @@ func drop(name string) {
|
|||||||
os.RemoveAll(appDataPath)
|
os.RemoveAll(appDataPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func autoBalloon(l *libvirt.Libvirt) {
|
func autoBalloon(l *libvirt.Libvirt, memoryMin, adjustPercent uint64) {
|
||||||
domains, err := l.Domains()
|
domains, err := l.Domains()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
const memoryMin = 512 * 1024 // 512 MiB
|
|
||||||
|
|
||||||
table := tablewriter.NewWriter(os.Stdout)
|
table := tablewriter.NewWriter(os.Stdout)
|
||||||
table.SetHeader([]string{"Application VM", "Used memory", "Current memory", "Max memory", "New memory"})
|
table.SetHeader([]string{"Application VM", "Used memory", "Current memory", "Max memory", "New memory"})
|
||||||
for _, d := range domains {
|
for _, d := range domains {
|
||||||
if d.Name[0:5] == "appvm" {
|
if strings.HasPrefix(d.Name, "appvm_") {
|
||||||
name := d.Name[6:]
|
name := d.Name[6:]
|
||||||
|
|
||||||
memoryUsedRaw, err := ioutil.ReadFile(os.Getenv("HOME") + "/appvm/" + name + "/.memory_used")
|
memoryUsedRaw, err := ioutil.ReadFile(os.Getenv("HOME") + "/appvm/" + name + "/.memory_used")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
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]))
|
memoryUsedMiB, err := strconv.Atoi(string(memoryUsedRaw[0 : len(memoryUsedRaw)-1]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Println(err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
memoryUsed := memoryUsedMiB * 1024
|
memoryUsed := memoryUsedMiB * 1024
|
||||||
|
|
||||||
_, memoryMax, memoryCurrent, _, _, err := l.DomainGetInfo(d)
|
_, memoryMax, memoryCurrent, _, _, err := l.DomainGetInfo(d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Println(err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
memoryNew := uint64(float64(memoryUsed) * 1.2) // +20%
|
memoryNew := uint64(float64(memoryUsed) * (1 + float64(adjustPercent)/100))
|
||||||
|
|
||||||
if memoryNew > memoryMax {
|
if memoryNew > memoryMax {
|
||||||
memoryNew = memoryMax - 1
|
memoryNew = memoryMax - 1
|
||||||
@ -282,7 +355,8 @@ func autoBalloon(l *libvirt.Libvirt) {
|
|||||||
|
|
||||||
err = l.DomainSetMemory(d, memoryNew)
|
err = l.DomainSetMemory(d, memoryNew)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Println(err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
table.Append([]string{name,
|
table.Append([]string{name,
|
||||||
@ -295,34 +369,172 @@ func autoBalloon(l *libvirt.Libvirt) {
|
|||||||
table.Render()
|
table.Render()
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func search(name string) {
|
||||||
c, err := net.DialTimeout("unix", "/var/run/libvirt/libvirt-sock", time.Second)
|
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 {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
l := libvirt.New(c)
|
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 {
|
if err := l.Connect(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
defer l.Disconnect()
|
defer l.Disconnect()
|
||||||
|
|
||||||
kingpin.Command("list", "List applications")
|
cleanupStatelessVMs(l)
|
||||||
kingpin.Command("autoballoon", "Automatically adjust/reduce app vm memory")
|
}
|
||||||
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() {
|
switch kingpin.Parse() {
|
||||||
case "list":
|
case "list":
|
||||||
list(l)
|
list(l)
|
||||||
|
case "search":
|
||||||
|
search(*searchName)
|
||||||
|
case "generate":
|
||||||
|
generate(*generateName, *generateBin, *generateVMName,
|
||||||
|
*generateBuildVM)
|
||||||
case "start":
|
case "start":
|
||||||
start(l, *startName)
|
networkModel := parseNetworkModel(*startOffline, *startNetwork)
|
||||||
|
start(l, *startName,
|
||||||
|
!*startQuiet, networkModel, !*startCli, *startStateless,
|
||||||
|
*startArgs, *startOpen)
|
||||||
case "stop":
|
case "stop":
|
||||||
stop(l, *stopName)
|
stop(l, *stopName)
|
||||||
case "drop":
|
case "drop":
|
||||||
drop(*dropName)
|
drop(*dropName)
|
||||||
case "autoballoon":
|
case "autoballoon":
|
||||||
autoBalloon(l)
|
autoBalloon(l, *minMemory*1024, *adjustPercent)
|
||||||
|
case "sync":
|
||||||
|
sync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
122
base.nix.go
Normal file
122
base.nix.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
var base_nix = `
|
||||||
|
{pkgs, ...}:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
<nix/local.nix>
|
||||||
|
];
|
||||||
|
|
||||||
|
services.xserver = {
|
||||||
|
enable = true;
|
||||||
|
desktopManager.xterm.enable = false;
|
||||||
|
displayManager = {
|
||||||
|
lightdm.enable = true;
|
||||||
|
autoLogin = {
|
||||||
|
enable = true;
|
||||||
|
user = "user";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
windowManager.xmonad.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
services.spice-vdagentd.enable = true;
|
||||||
|
|
||||||
|
users.extraUsers.user = {
|
||||||
|
uid = %s;
|
||||||
|
isNormalUser = true;
|
||||||
|
extraGroups = [ "audio" ];
|
||||||
|
createHome = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.etc."xmonad.hs".text = ''
|
||||||
|
import XMonad
|
||||||
|
main = xmonad def
|
||||||
|
{ workspaces = [ "" ]
|
||||||
|
, borderWidth = 0
|
||||||
|
, startupHook = startup
|
||||||
|
}
|
||||||
|
|
||||||
|
startup :: X ()
|
||||||
|
startup = do
|
||||||
|
spawn "while [ 1 ]; do ${pkgs.spice-vdagent}/bin/spice-vdagent -x; done &"
|
||||||
|
'';
|
||||||
|
|
||||||
|
systemd.services.home-user-build-xmonad = {
|
||||||
|
description = "Link xmonad configuration";
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = "/bin/sh -c 'mkdir -p /home/user/.xmonad && ln -sf /etc/xmonad.hs /home/user/.xmonad/xmonad.hs && /run/current-system/sw/bin/xmonad --recompile'";
|
||||||
|
RemainAfterExit = "yes";
|
||||||
|
User = "user";
|
||||||
|
Restart = "on-failure";
|
||||||
|
TimeoutSec = 10;
|
||||||
|
};
|
||||||
|
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 home /home/user'";
|
||||||
|
RemainAfterExit = "yes";
|
||||||
|
Type = "oneshot";
|
||||||
|
User = "root";
|
||||||
|
};
|
||||||
|
wantedBy = [ "sysinit.target" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.user.services."xrandr" = {
|
||||||
|
serviceConfig = {
|
||||||
|
StartLimitBurst = 100;
|
||||||
|
};
|
||||||
|
script = "${pkgs.xorg.xrandr}/bin/xrandr --output Virtual-1 --mode $(${pkgs.xorg.xrandr}/bin/xrandr | grep ' ' | head -n 2 | tail -n 1 | ${pkgs.gawk}/bin/awk '{ print $1 }')";
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.user.timers."xrandr" = {
|
||||||
|
description = "Auto update resolution crutch";
|
||||||
|
timerConfig = {
|
||||||
|
OnBootSec = "1s";
|
||||||
|
OnUnitInactiveSec = "1s";
|
||||||
|
Unit = "xrandr.service";
|
||||||
|
AccuracySec = "1us";
|
||||||
|
};
|
||||||
|
wantedBy = ["timers.target"];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services."autoballoon" = {
|
||||||
|
serviceConfig = {
|
||||||
|
StartLimitBurst = 100;
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
${pkgs.procps}/bin/free -m | grep Mem | \
|
||||||
|
${pkgs.gawk}/bin/awk '{print $2 "-" $4}' | \
|
||||||
|
${pkgs.bc}/bin/bc > /home/user/.memory_used
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.timers."autoballoon" = {
|
||||||
|
description = "Auto update resolution crutch";
|
||||||
|
timerConfig = {
|
||||||
|
OnBootSec = "1s";
|
||||||
|
OnUnitInactiveSec = "1s";
|
||||||
|
Unit = "autoballoon.service";
|
||||||
|
AccuracySec = "1us";
|
||||||
|
};
|
||||||
|
wantedBy = ["timers.target"];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func baseNix() []byte {
|
||||||
|
u, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return []byte(fmt.Sprintf(base_nix, u.Uid))
|
||||||
|
}
|
59
builtin.go
Normal file
59
builtin.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Builtin VMs
|
||||||
|
|
||||||
|
type app struct {
|
||||||
|
Name string
|
||||||
|
Nix []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var builtin_chromium_nix = app{
|
||||||
|
Name: "chromium",
|
||||||
|
Nix: []byte(`
|
||||||
|
{pkgs, ...}:
|
||||||
|
let
|
||||||
|
application = "${pkgs.chromium}/bin/chromium";
|
||||||
|
appRunner = pkgs.writeShellScriptBin "app" ''
|
||||||
|
ARGS_FILE=/home/user/.args
|
||||||
|
ARGS=$(cat $ARGS_FILE)
|
||||||
|
rm $ARGS_FILE
|
||||||
|
|
||||||
|
${application} $ARGS
|
||||||
|
systemctl poweroff
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
imports = [
|
||||||
|
<nixpkgs/nixos/modules/virtualisation/qemu-vm.nix>
|
||||||
|
<nix/base.nix>
|
||||||
|
];
|
||||||
|
|
||||||
|
programs.chromium = {
|
||||||
|
enable = true;
|
||||||
|
extensions = [
|
||||||
|
"cjpalhdlnbpafiamejdnhcphjbkeiagm" # uBlock Origin
|
||||||
|
"gcbommkclmclpchllfjekcdonpmejbdp" # HTTPS Everywhere
|
||||||
|
"fihnjjcciajhdojfnbdddfaoknhalnja" # I don't care about cookies
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
services.xserver.displayManager.sessionCommands = "${appRunner}/bin/app &";
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeBuiltinApps(path string) (err error) {
|
||||||
|
for _, f := range []app{
|
||||||
|
builtin_chromium_nix,
|
||||||
|
} {
|
||||||
|
err = ioutil.WriteFile(configDir+"/nix/"+f.Name+".nix", f.Nix, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
34
default.nix
Normal file
34
default.nix
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{ pkgs ? import <nixpkgs> {}, ... }:
|
||||||
|
let
|
||||||
|
virt-manager-without-menu = pkgs.virt-viewer.overrideAttrs (oldAttrs: {
|
||||||
|
patches = oldAttrs.patches ++ [
|
||||||
|
./patches/0001-Remove-menu-bar.patch
|
||||||
|
./patches/0002-Do-not-grab-keyboard-mouse.patch
|
||||||
|
./patches/0003-Use-name-of-appvm-applications-as-a-title.patch
|
||||||
|
./patches/0004-Use-title-application-name-as-subtitle.patch
|
||||||
|
];
|
||||||
|
});
|
||||||
|
in with pkgs;
|
||||||
|
|
||||||
|
buildGoModule rec {
|
||||||
|
pname = "appvm";
|
||||||
|
version = "master";
|
||||||
|
|
||||||
|
buildInputs = [ makeWrapper ];
|
||||||
|
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
vendorHash = "sha256-8eU+Mf5dxL/bAMMShXvj8I1Kdd4ysBTWvgYIXwLStPI=";
|
||||||
|
|
||||||
|
postFixup = ''
|
||||||
|
wrapProgram $out/bin/appvm \
|
||||||
|
--prefix PATH : "${lib.makeBinPath [ nix virt-manager-without-menu ]}"
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "Nix-based app VMs";
|
||||||
|
homepage = "https://code.dumpstack.io/tools/${pname}";
|
||||||
|
maintainers = [ lib.maintainers.dump_stack lib.maintainers.cab404 ];
|
||||||
|
license = lib.licenses.gpl3;
|
||||||
|
};
|
||||||
|
}
|
4
docs/architecture.rst
Normal file
4
docs/architecture.rst
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Architecture
|
||||||
|
============
|
||||||
|
|
||||||
|
*TODO*
|
30
docs/index.rst
Normal file
30
docs/index.rst
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
appvm
|
||||||
|
=====
|
||||||
|
|
||||||
|
*appvm* is the thin easy-to-use hypervisor-based sandboxing software.
|
||||||
|
|
||||||
|
*appvm* was created for the purpose of decreasing complexity of using
|
||||||
|
graphical software inside virtual machines, at the same time
|
||||||
|
maintaining reasonable security and keep *appvm* itself simple and
|
||||||
|
portable.
|
||||||
|
|
||||||
|
While I'm trying to keep that documentation up-to-date, there may be
|
||||||
|
some missing information. Use ``appvm --help-long`` for checking all
|
||||||
|
features.
|
||||||
|
|
||||||
|
If you found anything missed here, please make a pull request or send
|
||||||
|
patches to patch@dumpstack.io.
|
||||||
|
|
||||||
|
If you need personal support, your company is interested in the
|
||||||
|
project or you just want to share some thoughts -- feel free to create
|
||||||
|
a new issue or write to root@dumpstack.io.
|
||||||
|
|
||||||
|
Contents
|
||||||
|
========
|
||||||
|
|
||||||
|
:ref:`Keyword Index <genindex>`
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
|
||||||
|
architecture.rst
|
||||||
|
threat-model.rst
|
30
docs/installation.rst
Normal file
30
docs/installation.rst
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
NixOS
|
||||||
|
-----
|
||||||
|
|
||||||
|
virtualisation.appvm = {
|
||||||
|
enable = true;
|
||||||
|
user = "${username}";
|
||||||
|
};
|
||||||
|
|
||||||
|
Ubuntu 20.04
|
||||||
|
------
|
||||||
|
|
||||||
|
Requirements::
|
||||||
|
|
||||||
|
sudo apt install virt-manager curl git
|
||||||
|
echo user = "\"$USER\"" | sudo tee -a /etc/libvirt/qemu.conf
|
||||||
|
echo '/var/tmp/** rwlk,' | sudo tee -a /etc/apparmor.d/local/abstractions/libvirt-qemu
|
||||||
|
curl -L https://nixos.org/nix/install | sh
|
||||||
|
systemctl reboot
|
||||||
|
|
||||||
|
Use latest stable nixpkgs channel::
|
||||||
|
|
||||||
|
nix-channel --add https://nixos.org/channels/nixos-20.03 nixpkgs
|
||||||
|
nix-channel --update
|
||||||
|
|
||||||
|
Install appvm::
|
||||||
|
|
||||||
|
nix-env -if https://code.dumpstack.io/tools/appvm/archive/master.tar.gz
|
4
docs/threat-model.rst
Normal file
4
docs/threat-model.rst
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Threat model
|
||||||
|
=======================
|
||||||
|
|
||||||
|
*TODO*
|
88
dot-desktop-fuse/main.go
Normal file
88
dot-desktop-fuse/main.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ddf struct {
|
||||||
|
Path string
|
||||||
|
fs.Inode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ddf) OnAdd(ctx context.Context) {
|
||||||
|
files, err := ioutil.ReadDir(r.Path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
b, err := ioutil.ReadFile(filepath.Join(r.Path, file.Name()))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Contains(b, []byte("Actions=")) {
|
||||||
|
b = bytes.ReplaceAll(b, []byte("Actions="), []byte("Actions=appvm;"))
|
||||||
|
} else {
|
||||||
|
b = bytes.ReplaceAll(b, []byte("Exec="), []byte("Actions=appvm;\nExec="))
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := string(regexp.MustCompile("Exec=[a-zA-Z0-9]*").Find(b))
|
||||||
|
|
||||||
|
var app string
|
||||||
|
if len(raw) > 5 {
|
||||||
|
app = string(raw)[5:]
|
||||||
|
} else {
|
||||||
|
log.Println("Can't find Exec entry for", file.Name())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
b = append(b, []byte("\n[Desktop Action appvm]\n")...)
|
||||||
|
b = append(b, []byte("Name=Open in appvm\n")...)
|
||||||
|
b = append(b, []byte("Exec=appvm start "+app+"\n")...)
|
||||||
|
|
||||||
|
ch := r.NewPersistentInode(
|
||||||
|
ctx, &fs.MemRegularFile{
|
||||||
|
Data: b,
|
||||||
|
Attr: fuse.Attr{Mode: 0444},
|
||||||
|
}, fs.StableAttr{})
|
||||||
|
r.AddChild(file.Name(), ch, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = (fs.NodeOnAdder)((*ddf)(nil))
|
||||||
|
|
||||||
|
func setupSigintHandler(server *fuse.Server) {
|
||||||
|
c := make(chan os.Signal)
|
||||||
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-c
|
||||||
|
server.Unmount()
|
||||||
|
os.Exit(1)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
const from = "/var/run/current-system/sw/share/applications/"
|
||||||
|
|
||||||
|
to := filepath.Join(os.Getenv("HOME"), "/.local/share/applications")
|
||||||
|
os.MkdirAll(to, 0755)
|
||||||
|
|
||||||
|
server, err := fs.Mount(to, &ddf{Path: from}, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
setupSigintHandler(server)
|
||||||
|
server.Wait()
|
||||||
|
}
|
@ -1,16 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
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 "}"
|
|
205
generate.go
Normal file
205
generate.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var template = `
|
||||||
|
{pkgs, ...}:
|
||||||
|
let
|
||||||
|
application = "${pkgs.%s}/bin/%s";
|
||||||
|
appRunner = pkgs.writeShellScriptBin "app" ''
|
||||||
|
ARGS_FILE=/home/user/.args
|
||||||
|
ARGS=$(cat $ARGS_FILE)
|
||||||
|
rm $ARGS_FILE
|
||||||
|
|
||||||
|
${application} $ARGS
|
||||||
|
systemctl poweroff
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
imports = [
|
||||||
|
<nixpkgs/nixos/modules/virtualisation/qemu-vm.nix>
|
||||||
|
<nix/base.nix>
|
||||||
|
];
|
||||||
|
|
||||||
|
services.xserver.displayManager.sessionCommands = "${appRunner}/bin/app &";
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func isPackageExists(channel, name string) bool {
|
||||||
|
return nil == exec.Command("nix-build", "<"+channel+">", "-A", name).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func nixPath(name string) (path string, err error) {
|
||||||
|
command := exec.Command("nix", "path-info", name)
|
||||||
|
bytes, err := command.Output()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path = string(bytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func guessChannel() (channel string, err error) {
|
||||||
|
command := exec.Command("nix-channel", "--list")
|
||||||
|
bytes, err := command.Output()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channels := strings.Split(string(bytes), "\n")
|
||||||
|
for _, line := range channels {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) == 2 {
|
||||||
|
if strings.Contains(fields[1], "nixos.org/channels") {
|
||||||
|
channel = fields[0]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.New("No channel found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterDotfiles(files []os.FileInfo) (notHiddenFiles []os.FileInfo) {
|
||||||
|
for _, f := range files {
|
||||||
|
if !strings.HasPrefix(f.Name(), ".") {
|
||||||
|
notHiddenFiles = append(notHiddenFiles, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func generate(pkg, bin, vmname string, build bool) (err error) {
|
||||||
|
// TODO refactor
|
||||||
|
var name, channel string
|
||||||
|
|
||||||
|
if strings.Contains(pkg, ".") {
|
||||||
|
channel = strings.Split(pkg, ".")[0]
|
||||||
|
name = strings.Join(strings.Split(pkg, ".")[1:], ".")
|
||||||
|
} else {
|
||||||
|
log.Println("Package name does not contains channel")
|
||||||
|
log.Println("Trying to guess")
|
||||||
|
|
||||||
|
channel, err = guessChannel()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Cannot guess channel")
|
||||||
|
log.Println("Check nix-channel --list")
|
||||||
|
log.Println("Will try <nixpkgs>")
|
||||||
|
channel = "nixpkgs"
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
name = pkg
|
||||||
|
log.Println("Use", channel+"."+pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isPackageExists(channel, name) {
|
||||||
|
s := "Package " + name + " does not exists"
|
||||||
|
err = errors.New(s)
|
||||||
|
log.Println(s)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := nixPath(channel + "." + name)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Cannot find nix path")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path = strings.TrimSpace(path)
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir(path + "/bin/")
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if bin == "" && len(files) != 1 {
|
||||||
|
fmt.Println("There's more than one binary in */bin")
|
||||||
|
fmt.Println("Files in", path+"/bin/:")
|
||||||
|
for _, f := range files {
|
||||||
|
fmt.Println("\t", f.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Trying to guess binary")
|
||||||
|
var found bool = false
|
||||||
|
|
||||||
|
notHiddenFiles := filterDotfiles(files)
|
||||||
|
if len(notHiddenFiles) == 1 {
|
||||||
|
log.Println("Use", notHiddenFiles[0].Name())
|
||||||
|
bin = notHiddenFiles[0].Name()
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
for _, f := range files {
|
||||||
|
parts := strings.Split(pkg, ".")
|
||||||
|
if f.Name() == parts[len(parts)-1] {
|
||||||
|
log.Println("Use", f.Name())
|
||||||
|
bin = f.Name()
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
log.Println("Cannot guess in */bin, " +
|
||||||
|
"you should specify one of them explicitly")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bin != "" {
|
||||||
|
var found bool = false
|
||||||
|
for _, f := range files {
|
||||||
|
if bin == f.Name() {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
log.Println("There's no such file in */bin")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bin = files[0].Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
var appFilename string
|
||||||
|
if vmname != "" {
|
||||||
|
appFilename = configDir + "/nix/" + vmname + ".nix"
|
||||||
|
} else {
|
||||||
|
appFilename = configDir + "/nix/" + name + ".nix"
|
||||||
|
}
|
||||||
|
|
||||||
|
appNixConfig := fmt.Sprintf(template, name, bin)
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(appFilename, []byte(appNixConfig), 0600)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print(appNixConfig + "\n")
|
||||||
|
log.Println("Configuration file is saved to", appFilename)
|
||||||
|
|
||||||
|
if build {
|
||||||
|
if vmname != "" {
|
||||||
|
_, _, _, err = generateVM(configDir, vmname, true)
|
||||||
|
} else {
|
||||||
|
_, _, _, err = generateVM(configDir, name, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
14
go.mod
Normal file
14
go.mod
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
module code.dumpstack.io/tools/appvm
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||||
|
github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a // indirect
|
||||||
|
github.com/digitalocean/go-libvirt v0.0.0-20210723161134-761cfeeb5968
|
||||||
|
github.com/go-cmd/cmd v1.3.1
|
||||||
|
github.com/hanwen/go-fuse/v2 v2.1.0
|
||||||
|
github.com/jollheef/go-system v0.0.0-20160710075518-6ed6b1d2b8db
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||||
|
)
|
62
go.sum
Normal file
62
go.sum
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||||
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a h1:E/8AP5dFtMhl5KPJz66Kt9G0n+7Sn41Fy1wv9/jHOrc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/digitalocean/go-libvirt v0.0.0-20210723161134-761cfeeb5968 h1:ZdYBqLPrXioo+1Z97PWaTK4+jRcS45BI6JlepKtkPKI=
|
||||||
|
github.com/digitalocean/go-libvirt v0.0.0-20210723161134-761cfeeb5968/go.mod h1:o129ljs6alsIQTc8d6eweihqpmmrbxZ2g1jhgjhPykI=
|
||||||
|
github.com/go-cmd/cmd v1.3.1 h1:Scpez/YLL7xBmc1KRxDtHNXnamzQWqF4Sqy9SHnIMfE=
|
||||||
|
github.com/go-cmd/cmd v1.3.1/go.mod h1:VZqpYlBauogsSkJrj8NzQM6r/tztSewD/PfHCVjTdnA=
|
||||||
|
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
|
||||||
|
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||||
|
github.com/hanwen/go-fuse v1.0.0 h1:GxS9Zrn6c35/BnfiVsZVWmsG803xwE7eVRDvcf/BEVc=
|
||||||
|
github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok=
|
||||||
|
github.com/hanwen/go-fuse/v2 v2.1.0 h1:+32ffteETaLYClUj0a3aHjZ1hOPxxaNEHiZiujuDaek=
|
||||||
|
github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc=
|
||||||
|
github.com/jollheef/go-system v0.0.0-20160710075518-6ed6b1d2b8db h1:HGcWru24Gt24VFEsX7mxKtO+/NnKCuQ0LYtardulWMc=
|
||||||
|
github.com/jollheef/go-system v0.0.0-20160710075518-6ed6b1d2b8db/go.mod h1:Cj2JA+Wov6pwK3QTq2PuRXkZ5UM+DT3apJtBDUS8zKE=
|
||||||
|
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
|
||||||
|
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
|
||||||
|
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||||
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
|
||||||
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
7
local.nix.template.go
Normal file
7
local.nix.template.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
var local_nix_template = []byte(`
|
||||||
|
{
|
||||||
|
#services.xserver.xkbOptions = "ctrl:nocaps";
|
||||||
|
}
|
||||||
|
`)
|
73
nix/base.nix
73
nix/base.nix
@ -1,73 +0,0 @@
|
|||||||
{pkgs, ...}:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
<nix/monitor.nix>
|
|
||||||
<nix/local.nix>
|
|
||||||
];
|
|
||||||
|
|
||||||
system.nixos.stateVersion = "18.03";
|
|
||||||
|
|
||||||
services.xserver = {
|
|
||||||
enable = true;
|
|
||||||
desktopManager.xterm.enable = false;
|
|
||||||
displayManager.slim = {
|
|
||||||
enable = true;
|
|
||||||
defaultUser = "user";
|
|
||||||
autoLogin = true;
|
|
||||||
};
|
|
||||||
windowManager.xmonad.enable = true;
|
|
||||||
windowManager.default = "xmonad";
|
|
||||||
};
|
|
||||||
|
|
||||||
services.spice-vdagentd.enable = true;
|
|
||||||
|
|
||||||
users.extraUsers.user = {
|
|
||||||
isNormalUser = true;
|
|
||||||
extraGroups = [ "audio" ];
|
|
||||||
createHome = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
environment.etc."xmonad.hs".text = ''
|
|
||||||
import XMonad
|
|
||||||
main = xmonad defaultConfig
|
|
||||||
{ workspaces = [ "" ]
|
|
||||||
, borderWidth = 0
|
|
||||||
, startupHook = startup
|
|
||||||
}
|
|
||||||
|
|
||||||
startup :: X ()
|
|
||||||
startup = do
|
|
||||||
spawn "spice-vdagent"
|
|
||||||
'';
|
|
||||||
|
|
||||||
environment.systemPackages = [ pkgs.bc ];
|
|
||||||
services.cron = {
|
|
||||||
enable = true;
|
|
||||||
systemCronJobs = [
|
|
||||||
"* * * * * root free -m | grep Mem | awk '{print $2 \"-\" $4}' | bc > /home/user/.memory_used"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.home-user-build-xmonad = {
|
|
||||||
description = "Create and xmonad configuration";
|
|
||||||
serviceConfig = {
|
|
||||||
ConditionFileNotEmpty = "!/home/user/.xmonad/xmonad.hs";
|
|
||||||
ExecStart = "/bin/sh -c 'mkdir -p /home/user/.xmonad && ln -s /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" ];
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
{pkgs, ...}:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
<nixpkgs/nixos/modules/virtualisation/qemu-vm.nix>
|
|
||||||
<nix/base.nix>
|
|
||||||
];
|
|
||||||
|
|
||||||
environment.etc."chromium/policies/managed/plugins.json".text = ''
|
|
||||||
{
|
|
||||||
"ExtensionInstallForcelist": [
|
|
||||||
// uBlock Origin (https://chrome.google.com/webstore/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm)
|
|
||||||
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
|
|
||||||
// HTTPS Everywhere (https://chrome.google.com/webstore/detail/https-everywhere/gcbommkclmclpchllfjekcdonpmejbdp)
|
|
||||||
"gcbommkclmclpchllfjekcdonpmejbdp;https://clients2.google.com/service/update2/crx",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
|
|
||||||
environment.systemPackages = [ pkgs.chromium ];
|
|
||||||
services.xserver.displayManager.sessionCommands = "while [ 1 ]; do ${pkgs.chromium}/bin/chromium; done &";
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
{pkgs, ...}:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
<nixpkgs/nixos/modules/virtualisation/qemu-vm.nix>
|
|
||||||
<nix/base.nix>
|
|
||||||
];
|
|
||||||
|
|
||||||
environment.systemPackages = [ pkgs.evince ];
|
|
||||||
services.xserver.displayManager.sessionCommands = "while [ 1 ]; do ${pkgs.evince}/bin/evince; done &";
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
{pkgs, ...}:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
<nixpkgs/nixos/modules/virtualisation/qemu-vm.nix>
|
|
||||||
<nix/base.nix>
|
|
||||||
];
|
|
||||||
|
|
||||||
environment.systemPackages = [ pkgs.libreoffice ];
|
|
||||||
services.xserver.displayManager.sessionCommands = "while [ 1 ]; do ${pkgs.libreoffice}/bin/soffice; done &";
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
services.xserver.layout = "us,ru";
|
|
||||||
services.xserver.xkbOptions = "ctrl:nocaps,grp:rctrl_toggle";
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
services.xserver.monitorSection = ''
|
|
||||||
Modeline "1920x1080_60.00" 173.00 1920 2048 2248 2576 1080 1083 1088 1120 -hsync +vsync
|
|
||||||
Option "PreferredMode" "1920x1080_60.00"
|
|
||||||
'';
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
{pkgs, ...}:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
<nixpkgs/nixos/modules/virtualisation/qemu-vm.nix>
|
|
||||||
<nix/base.nix>
|
|
||||||
];
|
|
||||||
|
|
||||||
environment.systemPackages = [ pkgs.tdesktop ];
|
|
||||||
services.xserver.displayManager.sessionCommands = "while [ 1 ]; do ${pkgs.tdesktop}/bin/telegram-desktop; done &";
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
{pkgs, ...}:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
<nixpkgs/nixos/modules/virtualisation/qemu-vm.nix>
|
|
||||||
<nix/base.nix>
|
|
||||||
];
|
|
||||||
|
|
||||||
environment.systemPackages = [ pkgs.thunderbird ];
|
|
||||||
services.xserver.displayManager.sessionCommands = "while [ 1 ]; do ${pkgs.thunderbird}/bin/thunderbird; done &";
|
|
||||||
}
|
|
10
nix/wire.nix
10
nix/wire.nix
@ -1,10 +0,0 @@
|
|||||||
{pkgs, ...}:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
<nixpkgs/nixos/modules/virtualisation/qemu-vm.nix>
|
|
||||||
<nix/base.nix>
|
|
||||||
];
|
|
||||||
|
|
||||||
environment.systemPackages = [ pkgs.wire-desktop ];
|
|
||||||
services.xserver.displayManager.sessionCommands = "while [ 1 ]; do ${pkgs.wire-desktop}/bin/wire-desktop; done &";
|
|
||||||
}
|
|
44
nixos/default.nix
Normal file
44
nixos/default.nix
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
params@{ config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.virtualisation.appvm;
|
||||||
|
appvm = import ../. params;
|
||||||
|
in with lib; {
|
||||||
|
|
||||||
|
options = {
|
||||||
|
virtualisation.appvm = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
This enables AppVMs and related virtualisation settings.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = ''
|
||||||
|
AppVM user login. Currenly only AppVMs are supported for a single user only.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
virtualisation.libvirtd = {
|
||||||
|
enable = true;
|
||||||
|
qemuVerbatimConfig = ''
|
||||||
|
namespaces = []
|
||||||
|
user = "${cfg.user}"
|
||||||
|
group = "users"
|
||||||
|
remember_owner = 0
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users."${cfg.user}" = {
|
||||||
|
packages = [ appvm ];
|
||||||
|
extraGroups = [ "libvirtd" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
4
os/.gitignore
vendored
Normal file
4
os/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
*.iso
|
||||||
|
target.nix
|
||||||
|
result
|
||||||
|
nixos.qcow2
|
20
os/Makefile
Normal file
20
os/Makefile
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
test: vm cleanup
|
||||||
|
|
||||||
|
vm:
|
||||||
|
ln -sf vm.nix target.nix
|
||||||
|
nix-build '<nixpkgs/nixos>' -A vm -I nixos-config=configuration.nix
|
||||||
|
@echo "Use Ctrl-Alt-Q to close VM" | grep --color=always '.*'
|
||||||
|
./result/bin/run-nixos-vm -cpu host
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
rm -f nixos.qcow2
|
||||||
|
unlink result
|
||||||
|
|
||||||
|
iso:
|
||||||
|
@echo "Not yet available. Use \`make live-iso\`." | grep --color=always '.*'
|
||||||
|
|
||||||
|
live-iso:
|
||||||
|
ln -sf live-iso.nix target.nix
|
||||||
|
nix-build '<nixpkgs/nixos>' -A config.system.build.isoImage -I nixos-config=configuration.nix
|
||||||
|
cp result/iso/* appvm.iso
|
||||||
|
unlink result
|
11
os/README.md
Normal file
11
os/README.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# $Placeholder OS
|
||||||
|
|
||||||
|
The primary goal of appvm is to provide application VMs as a tool, but some people ask for a complete distro so why not.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
make test
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
make live-iso
|
76
os/configuration.nix
Normal file
76
os/configuration.nix
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
appvm = (import ../default.nix);
|
||||||
|
in {
|
||||||
|
imports = [
|
||||||
|
<nixpkgs/nixos/modules/installer/cd-dvd/channel.nix>
|
||||||
|
./target.nix
|
||||||
|
#./hardware-configuration.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
time.timeZone = "UTC";
|
||||||
|
|
||||||
|
boot.loader.systemd-boot.enable = true;
|
||||||
|
|
||||||
|
# You can not use networking.networkmanager with networking.wireless
|
||||||
|
networking.wireless.enable = false;
|
||||||
|
|
||||||
|
users.users.user = {
|
||||||
|
isNormalUser = true;
|
||||||
|
initialPassword = "user"; # should be changed right after start
|
||||||
|
extraGroups = [ "audio" "libvirtd" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
virtualisation.libvirtd = {
|
||||||
|
enable = true;
|
||||||
|
qemuVerbatimConfig = ''
|
||||||
|
namespaces = []
|
||||||
|
user = "user"
|
||||||
|
group = "users"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.user.services."dot-desktop-fuse" = {
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = "${appvm}/bin/dot-desktop-fuse";
|
||||||
|
Restart = "on-failure";
|
||||||
|
};
|
||||||
|
path = [ "/run/wrappers" ];
|
||||||
|
wantedBy = [ "default.target" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.user.services."autoballoon" = {
|
||||||
|
serviceConfig.StartLimitBurst = 64;
|
||||||
|
script = "${appvm}/bin/appvm autoballoon";
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.user.timers."autoballoon" = {
|
||||||
|
description = "Autoupdate resolution crutch";
|
||||||
|
timerConfig = {
|
||||||
|
OnBootSec = "1s";
|
||||||
|
OnUnitInactiveSec = "1s";
|
||||||
|
Unit = "autoballoon.service";
|
||||||
|
AccuracySec = "1us";
|
||||||
|
};
|
||||||
|
wantedBy = ["timers.target"];
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
appvm virtmanager chromium
|
||||||
|
# Cache packages required for application VMs
|
||||||
|
xmonad-with-packages spice-vdagent bc qemu_test lightdm
|
||||||
|
];
|
||||||
|
|
||||||
|
services.xserver.enable = true;
|
||||||
|
services.xserver.displayManager.gdm = {
|
||||||
|
enable = true;
|
||||||
|
wayland = false; # FIXME
|
||||||
|
autoLogin = {
|
||||||
|
enable = true;
|
||||||
|
user = "user";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.xserver.desktopManager.gnome3.enable = true;
|
||||||
|
}
|
6
os/live-iso.nix
Normal file
6
os/live-iso.nix
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
imports = [
|
||||||
|
#<nixpkgs/nixos/modules/profiles/hardened.nix>
|
||||||
|
<nixpkgs/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix>
|
||||||
|
];
|
||||||
|
}
|
5
os/vm.nix
Normal file
5
os/vm.nix
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
# vm.nix is used for testing only
|
||||||
|
users.users.root.initialPassword = "root";
|
||||||
|
virtualisation.memorySize = 8196;
|
||||||
|
}
|
12
patches/0001-Remove-menu-bar.patch
Normal file
12
patches/0001-Remove-menu-bar.patch
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
diff --git a/src/resources/ui/virt-viewer.ui b/src/resources/ui/virt-viewer.ui
|
||||||
|
index 430f879..68856fc 100644
|
||||||
|
--- a/src/resources/ui/virt-viewer.ui
|
||||||
|
+++ b/src/resources/ui/virt-viewer.ui
|
||||||
|
@@ -137,7 +137,6 @@
|
||||||
|
</child>
|
||||||
|
<child type="titlebar">
|
||||||
|
<object class="GtkHeaderBar" id="header">
|
||||||
|
- <property name="visible">True</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="show-close-button">True</property>
|
||||||
|
<child>
|
15
patches/0002-Do-not-grab-keyboard-mouse.patch
Normal file
15
patches/0002-Do-not-grab-keyboard-mouse.patch
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
diff --git a/src/virt-viewer-display-spice.c b/src/virt-viewer-display-spice.c
|
||||||
|
index 2265f02..1d60d81 100644
|
||||||
|
--- a/src/virt-viewer-display-spice.c
|
||||||
|
+++ b/src/virt-viewer-display-spice.c
|
||||||
|
@@ -317,8 +317,8 @@ virt_viewer_display_spice_new(VirtViewerSessionSpice *session,
|
||||||
|
gtk_container_add(GTK_CONTAINER(self), GTK_WIDGET(self->display));
|
||||||
|
gtk_widget_show(GTK_WIDGET(self->display));
|
||||||
|
g_object_set(self->display,
|
||||||
|
- "grab-keyboard", TRUE,
|
||||||
|
- "grab-mouse", TRUE,
|
||||||
|
+ "grab-keyboard", FALSE,
|
||||||
|
+ "grab-mouse", FALSE,
|
||||||
|
"resize-guest", FALSE,
|
||||||
|
"scaling", TRUE,
|
||||||
|
NULL);
|
14
patches/0003-Use-name-of-appvm-applications-as-a-title.patch
Normal file
14
patches/0003-Use-name-of-appvm-applications-as-a-title.patch
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
diff --git a/src/virt-viewer-window.c b/src/virt-viewer-window.c
|
||||||
|
index fe740ce..d45fd4f 100644
|
||||||
|
--- a/src/virt-viewer-window.c
|
||||||
|
+++ b/src/virt-viewer-window.c
|
||||||
|
@@ -1342,6 +1342,9 @@ virt_viewer_window_update_title(VirtViewerWindow *self)
|
||||||
|
grabhint,
|
||||||
|
g_get_application_name());
|
||||||
|
}
|
||||||
|
+ } else if (g_str_has_prefix(self->subtitle, "appvm_")) {
|
||||||
|
+ /* Use name of the application as a title */
|
||||||
|
+ title = g_strdup_printf(_("%s"), &self->subtitle[strlen("appvm_")]);
|
||||||
|
} else if (self->subtitle) {
|
||||||
|
/* translators:
|
||||||
|
* This is "<subtitle> - <appname>"
|
12
patches/0004-Use-title-application-name-as-subtitle.patch
Normal file
12
patches/0004-Use-title-application-name-as-subtitle.patch
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
diff --git a/src/virt-viewer-app.c b/src/virt-viewer-app.c
|
||||||
|
index b977b7b..36bce34 100644
|
||||||
|
--- a/src/virt-viewer-app.c
|
||||||
|
+++ b/src/virt-viewer-app.c
|
||||||
|
@@ -976,6 +976,7 @@ virt_viewer_app_set_window_subtitle(VirtViewerApp *app,
|
||||||
|
*d = '%';
|
||||||
|
} else
|
||||||
|
subtitle = g_strdup_printf("%s (%s)", title, desc);
|
||||||
|
+ subtitle = g_strdup_printf("%s", title);
|
||||||
|
g_free(desc);
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 509 KiB |
4
shell.nix
Normal file
4
shell.nix
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
|
with pkgs; mkShell {
|
||||||
|
buildInputs = [ go virt-viewer virt-manager ];
|
||||||
|
}
|
115
xml.go
Normal file
115
xml.go
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// You may think that you want to rewrite to proper golang structures.
|
||||||
|
// Believe me, you shouldn't.
|
||||||
|
|
||||||
|
func generateXML(vmName string, network networkModel, gui bool,
|
||||||
|
vmNixPath, reginfo, img, sharedDir string) string {
|
||||||
|
|
||||||
|
devices := ""
|
||||||
|
|
||||||
|
if gui {
|
||||||
|
devices = guiDevices
|
||||||
|
}
|
||||||
|
|
||||||
|
qemuParams := qemuParamsDefault
|
||||||
|
|
||||||
|
if network == networkQemu {
|
||||||
|
qemuParams = qemuParamsWithNetwork
|
||||||
|
} else if network == networkLibvirt {
|
||||||
|
devices += netDevices
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(xmlTmpl, vmName, vmNixPath, vmNixPath, vmNixPath,
|
||||||
|
reginfo, img, sharedDir, sharedDir, sharedDir, devices, qemuParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
var qemuParamsDefault = `
|
||||||
|
<qemu:commandline>
|
||||||
|
<qemu:arg value='-snapshot'/>
|
||||||
|
</qemu:commandline>
|
||||||
|
`
|
||||||
|
|
||||||
|
var qemuParamsWithNetwork = `
|
||||||
|
<qemu:commandline>
|
||||||
|
<qemu:arg value='-device'/>
|
||||||
|
<qemu:arg value='e1000,netdev=net0,bus=pci.0,addr=0x10'/>
|
||||||
|
<qemu:arg value='-netdev'/>
|
||||||
|
<qemu:arg value='user,id=net0'/>
|
||||||
|
<qemu:arg value='-snapshot'/>
|
||||||
|
</qemu:commandline>
|
||||||
|
`
|
||||||
|
|
||||||
|
var netDevices = `
|
||||||
|
<interface type='network'>
|
||||||
|
<source network='default'/>
|
||||||
|
</interface>
|
||||||
|
`
|
||||||
|
|
||||||
|
var guiDevices = `
|
||||||
|
<!-- Graphical console -->
|
||||||
|
<graphics type='spice' autoport='yes'>
|
||||||
|
<listen type='address'/>
|
||||||
|
<image compression='off'/>
|
||||||
|
</graphics>
|
||||||
|
<!-- Guest additionals support -->
|
||||||
|
<channel type='spicevmc'>
|
||||||
|
<target type='virtio' name='com.redhat.spice.0'/>
|
||||||
|
</channel>
|
||||||
|
<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>
|
||||||
|
`
|
||||||
|
|
||||||
|
var xmlTmpl = `
|
||||||
|
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
|
||||||
|
<name>%s</name>
|
||||||
|
<memory unit='GiB'>2</memory>
|
||||||
|
<currentMemory unit='GiB'>1</currentMemory>
|
||||||
|
<vcpu>4</vcpu>
|
||||||
|
<os>
|
||||||
|
<type arch='x86_64'>hvm</type>
|
||||||
|
<kernel>%s/kernel</kernel>
|
||||||
|
<initrd>%s/initrd</initrd>
|
||||||
|
<cmdline>loglevel=4 init=%s/init %s</cmdline>
|
||||||
|
</os>
|
||||||
|
<features>
|
||||||
|
<acpi></acpi>
|
||||||
|
</features>
|
||||||
|
<clock offset='utc'/>
|
||||||
|
<on_poweroff>destroy</on_poweroff>
|
||||||
|
<on_reboot>restart</on_reboot>
|
||||||
|
<on_crash>destroy</on_crash>
|
||||||
|
<devices>
|
||||||
|
<!-- 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>
|
||||||
|
<!-- filesystems -->
|
||||||
|
<filesystem type='mount' accessmode='passthrough'>
|
||||||
|
<source dir='/nix/store'/>
|
||||||
|
<target dir='nix-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>
|
||||||
|
%s
|
||||||
|
</devices>
|
||||||
|
%s
|
||||||
|
</domain>
|
||||||
|
`
|
Reference in New Issue
Block a user