Compare commits

...

150 Commits
v0.1 ... master

Author SHA1 Message Date
Jonas Heinrich 17f17be784 fix ethernet device pci slot conflict 2022-05-23 19:13:51 +00:00
Mikhail Klementev 467f74f19a
Update README.md 2022-05-23 19:10:39 +00:00
Mikhail Klementev ce92ddf1b9
Make sure xmonad always uses the latest libc 2021-12-20 13:48:24 +00:00
Mikhail Klementev d698f058ff
Fix display manager configuration for 21.11 2021-12-20 13:47:03 +00:00
Mikhail Klementev 84374314a2
Use per-VM qcow2, remove it after the start 2021-12-20 13:44:20 +00:00
Mikhail Klementev a3bd38c243
gofmt 2021-12-20 13:42:40 +00:00
ilian 5753321877 Fix 9p mount of /nix/store
This fixes the following error during stage 1:
9pnet_virtio: no channels available for device nix-store
2021-12-20 13:40:52 +00:00
ilian c6159a7925 Update vendorSha256 2021-12-20 13:40:52 +00:00
ilian 2560e852df Remove deprecated goPackagePath attribute
Building otherwise fails with the following error:
`goPackagePath` is not needed with `buildGoModule`
2021-12-20 13:40:52 +00:00
ilian f063f2a90c docs/installation: fix typo 2021-12-20 13:40:52 +00:00
Mikhail Klementev 33facb2321
Update go-libvirt
Resolves #26
2021-11-04 13:24:32 +00:00
Mikhail Klementev 9142fe53c6
Remove donate 2020-12-16 16:51:48 +00:00
Vladimir Serov c2413d0208 Updated installation instructions for NixOS 2020-07-14 09:49:49 +00:00
Vladimir Serov d9c651987b fix: stat made qcow2 image creation return error every time 2020-07-14 09:49:49 +00:00
Vladimir Serov 3483763938 ooops, nixos cannot compute 2020-07-14 09:49:49 +00:00
Vladimir Serov bfc28be996 fix for read-write on initrd
https://www.redhat.com/archives/libvir-list/2020-July/msg00451.html
2020-07-14 09:49:49 +00:00
Vladimir Serov 6321004848 added forgotten group to our user 2020-07-14 09:49:49 +00:00
Vladimir Serov 5376e7a56f nixos configuration module 2020-07-14 09:49:49 +00:00
Vladimir Serov 77eada72c3 nixpkgs-able default.nix 2020-07-14 09:49:49 +00:00
Vladimir Serov 9a602a2231 gotta maintain da project 2020-07-14 09:49:49 +00:00
Mikhail Klementev e6f086f88a
Switch to buildGoModule 2020-06-18 16:08:56 +00:00
Mikhail Klementev 8264f71eb2
Follow redirect 2020-06-18 15:27:16 +00:00
msm-code ba80fba648
Add --network switch to select a networking model (#22) 2020-04-06 09:12:46 +00:00
msm-code e588479701
Add `--cli` switch that disables the GUI window. (#20) 2020-04-05 14:47:10 +00:00
msm-code a583335865
Allow VMs with hostname other than default (#19)
The VM runner created by nix is called `./run-<hostname>-vm`.
So far, the code hardcoded it as `./result/bin/run-nixos-vm`, but
for the users that want to change VM hostname it may be problematic.
2020-04-05 13:30:48 +00:00
Mikhail Klementev aaede60c4d
Switch to NixOS 20.03 2020-04-01 07:39:23 +00:00
Mikhail Klementev 6e26754cec
Do not specify session explicitly 2020-04-01 07:39:04 +00:00
msm-code c0801541b3
Fix random panics during autoballonning. (#17)
There is a race condition, when good behaving VMs will temporarily
truncate .memory_used to 0 bytes (update is not atomic).

Also, as a matter of principle, VMs should not crash the host,
ever.

Co-authored-by: msm <msm@cert.pl>
2020-03-30 21:53:02 +00:00
Mikhail Klementev 63cd3fc79d
GitHub Actions: Use latest stable nixpkgs channel 2020-02-21 00:29:49 +00:00
Mikhail Klementev 00f705d757
Update screenshot 2020-02-02 02:55:37 +00:00
Mikhail Klementev 218e81a6ac
virt-viewer: use title as subtitle 2020-02-02 02:13:57 +00:00
Mikhail Klementev 7cad1efce8
Move installation guide to docs 2020-02-02 01:43:35 +00:00
Mikhail Klementev cc5262b10f
Execute GitHub action on pull requests too 2020-01-31 08:03:05 +00:00
Mikhail Klementev e02edbacf4
Use default machine 2020-01-28 00:12:48 +00:00
Mikhail Klementev 2bc5247ac1
Note about the group name
Resolves #14
2020-01-26 19:15:53 +00:00
Mikhail Klementev 545291b9cf
Suggest to use latest stable channel 2020-01-26 10:23:33 +00:00
Mikhail Klementev 9b828ff575
Update installation guide 2020-01-26 01:39:36 +00:00
Mikhail Klementev dc636c3919
Revert "virt-manager: disable subtitle"
This reverts commit d07d51c7ec.
2020-01-22 02:33:02 +00:00
Mikhail Klementev d07d51c7ec
virt-manager: disable subtitle 2020-01-22 02:29:54 +00:00
Mikhail Klementev 19b92aec1b
virt-viewer: use name of appvm applications as a title 2020-01-22 02:21:19 +00:00
Mikhail Klementev 680aff06f7
Build appvm from ../default.nix 2020-01-22 01:18:25 +00:00
Mikhail Klementev 7b76368699
Cleanup 2020-01-22 01:13:26 +00:00
Mikhail Klementev 5a2ce12c2f
Build from the source directory 2020-01-22 01:13:05 +00:00
Mikhail Klementev f8bfca0466
GitHub Actions: Do not generate ISO 2020-01-22 00:59:46 +00:00
Mikhail Klementev 01a0d206db
virt-viewer: do not grab keyboard/mouse 2020-01-22 00:50:54 +00:00
Mikhail Klementev 44cb4697ec
Remove virt-viewer menu bar 2020-01-22 00:33:20 +00:00
Mikhail Klementev 219c48b976
Add dashboard access token 2020-01-20 09:22:40 +00:00
Mikhail Klementev c68f3bf10d
Donations 2020-01-16 23:50:39 +00:00
Mikhail Klementev 8b271a3e5e
Copy templates at start of tool not vm 2020-01-09 23:31:12 +00:00
Mikhail Klementev 2146397906
Avoid of changing current work directory 2020-01-09 23:13:48 +00:00
Mikhail Klementev 1fbbdee76a
Handle case when there's no actions in .desktop file 2020-01-09 23:07:37 +00:00
Mikhail Klementev 1f4214199e
Build appvm iso with the same source code not master 2020-01-09 23:02:00 +00:00
Mikhail Klementev 4e7c6e9895
Build ISO only on master 2020-01-09 23:01:03 +00:00
Mikhail Klementev 88718323f0
Add flag for build vm to `appvm generate` 2020-01-09 22:59:45 +00:00
Mikhail Klementev 4ce1c5643f
Ignore iso files 2020-01-08 22:25:48 +00:00
Mikhail Klementev 9c9a71c2c8
Set default password for user 2020-01-08 22:24:11 +00:00
Mikhail Klementev fca2f89046
Support remove run with `nix run` 2020-01-08 20:52:29 +00:00
Mikhail Klementev bd63fcd48f
Add dot-desktop-fuse service 2020-01-08 11:37:45 +00:00
Mikhail Klementev f15d0fb07a
Use master instead of latest tag 2020-01-08 11:20:26 +00:00
Mikhail Klementev a264cb2ced
Add missed deps 2020-01-08 11:16:14 +00:00
Mikhail Klementev 33cdae7a47
go modules init for dot-desktop-fuse 2020-01-08 01:44:41 +00:00
Mikhail Klementev 41f25418e8
Initial implementation for .desktop fuse fs 2020-01-07 21:22:44 +00:00
Mikhail Klementev 2be0f82e4a
User host user id 2020-01-07 20:01:52 +00:00
Mikhail Klementev dad147eb91
Do not exit on autoballoon errors, keep adjust memory for other VMs 2020-01-05 09:08:43 +00:00
Mikhail Klementev 806448df17
Memory autoballoon (host side) 2020-01-05 09:02:03 +00:00
Mikhail Klementev 573be66059
Do no enable anything in default local.nix 2020-01-05 08:55:36 +00:00
Mikhail Klementev 7df36ae8fc
Add channel snapshot to image itself 2020-01-05 08:49:48 +00:00
Mikhail Klementev 4066e347ef
GitHub Actions: iso artifact 2020-01-05 07:02:34 +00:00
Mikhail Klementev de77abd3ee
slim -> lightdm 2020-01-05 06:58:46 +00:00
Mikhail Klementev cead5bab74
GitHub Actions: Live ISO 2020-01-05 06:56:47 +00:00
Mikhail Klementev 6536200669
Use lightdm (slim is removed from NixOS) 2020-01-05 06:43:55 +00:00
Mikhail Klementev 1095218222
GitHub Actions: Nix environment 2020-01-05 06:20:51 +00:00
Mikhail Klementev a4138a4b44
Add Nix build job 2020-01-05 06:13:56 +00:00
Mikhail Klementev 287c6d6043
Update .gitignore 2020-01-05 06:12:18 +00:00
Mikhail Klementev 477b48d783
Use `go build` by default 2020-01-05 06:12:05 +00:00
Mikhail Klementev 843ca6864e
Refactor 2020-01-05 06:07:02 +00:00
Mikhail Klementev 02832a6e38
Add Documentation Status 2020-01-05 06:03:59 +00:00
Mikhail Klementev 3f8aa0cc59
Init docs 2020-01-05 05:57:55 +00:00
Mikhail Klementev 5526cbee02
Initial implementation of appvm-based OS 2020-01-04 20:58:00 +00:00
Mikhail Klementev 7d6d524b67
Add Nix package 2020-01-04 18:20:27 +00:00
Mikhail Klementev 40ef3fe50e
Use only nixos.org channels if guess 2020-01-04 11:23:40 +00:00
Mikhail Klementev d973179557
Do not install to user profile 2020-01-04 10:52:16 +00:00
Mikhail Klementev 49b8790032
Add TODO 2020-01-03 23:01:23 +00:00
Mikhail Klementev 355fb314a1
Implements stateless VMs 2020-01-03 22:56:27 +00:00
Mikhail Klementev e037770c38
Revert "Do not install apps to nix user profile"
This reverts commit 02dda8bcf9.
2020-01-03 22:42:27 +00:00
Mikhail Klementev 57e15fa0a0
Move shared directory creation out of vm generating function 2020-01-03 22:10:18 +00:00
Mikhail Klementev f1fd2e1505
spice-vdagent should be alive 2020-01-03 10:14:28 +00:00
Mikhail Klementev 02dda8bcf9
Do not install apps to nix user profile 2020-01-03 09:36:39 +00:00
Mikhail Klementev 13226a6a79
Implements command line flag to disable internet connection 2020-01-02 20:34:35 +00:00
Mikhail Klementev 0c35a66606
Implements passing file/arguments to application 2020-01-02 20:22:11 +00:00
Mikhail Klementev dbfc2929db
Update builtin chromium appvm description 2020-01-02 17:52:22 +00:00
Mikhail Klementev 3e8a08d638
Shutdown vm at app close, initial support for args 2020-01-02 17:44:46 +00:00
Mikhail Klementev 8baa1ff73b
Remove excess logging 2020-01-02 17:13:16 +00:00
Mikhail Klementev 25a2f45e59
Fix generating for packages inside modules 2020-01-02 17:09:58 +00:00
Mikhail Klementev af5691b0d4
Implements update repos wrapper 2020-01-02 15:47:42 +00:00
Mikhail Klementev 6c72b5de00
Implements wrapper for search for apps 2020-01-02 15:37:32 +00:00
Mikhail Klementev 04b2cf63ce
Generate appvm at start automatically if not exists 2020-01-02 15:29:20 +00:00
Mikhail Klementev 7e4aa33a0a
Set default mode to verbose 2020-01-02 15:20:22 +00:00
Mikhail Klementev bc704df503
Auto guess binary file name 2020-01-02 15:16:06 +00:00
Mikhail Klementev 43d42c2242
go modules 2019-12-30 15:55:00 +00:00
Mikhail Klementev f4c6a6a90b
Update README.md 2019-12-30 14:18:34 +00:00
Mikhail Klementev 0a51db3bde
Do not fetch remote repository 2019-12-30 14:15:59 +00:00
Mikhail Klementev 99bd71b80c
Embed builtin description for apps 2019-12-30 14:09:44 +00:00
Mikhail Klementev 3416808444
Remove *nix for apps that can be generated by 'appvm generate' 2019-12-30 14:01:50 +00:00
Mikhail Klementev 9c6b8a0122
Custom name for generate VMs 2019-12-30 13:58:18 +00:00
Mikhail Klementev cc92ec3e23
Show local apps as available 2019-12-30 13:52:41 +00:00
Mikhail Klementev 6ebc562599
Save app nix description to config directory 2019-12-30 13:41:33 +00:00
Mikhail Klementev f18d55bd27
New command: automatic generate app description 2019-12-30 10:47:47 +00:00
Vladimir Serov 197a78f595
[fix] rename miss 2019-12-30 00:24:01 +03:00
Vladimir Serov 555768d03b
Merge branch 'master' of github.com:jollheef/appvm 2019-12-30 00:23:37 +03:00
Vladimir Serov 4104d91eab
[feat] local config evaluation; getting repos while using remote configs. 2019-12-30 00:21:42 +03:00
Mikhail Klementev 2d5b72219f
GitHub Actions: Fix fetch dependencies 2019-12-29 19:31:12 +00:00
Mikhail Klementev 84095193e5
GitHub Actions: Fetch dependencies 2019-12-29 19:28:59 +00:00
Mikhail Klementev 718ab3a164
Remove outdated comment 2019-12-29 19:24:00 +00:00
Mikhail Klementev 6b35acf673
Add GitHub actions 2019-12-29 19:21:40 +00:00
Mikhail Klementev 38a3ba301b
Update README.md 2019-12-29 19:10:57 +00:00
Mikhail Klementev 29e447435f
Refactor 2019-12-29 19:03:17 +00:00
Mikhail Klementev 8a93015bfe
Update used memory each second (minute previously)
Resolves #4
2019-12-29 18:57:36 +00:00
Mikhail Klementev e7cc81694b
Remove outdated TODO
Resolves #8
2019-12-29 18:42:19 +00:00
Mikhail Klementev 6505544535
We're not store base/local configuration in nix/* anymore 2019-12-29 17:59:09 +00:00
Mikhail Klementev 24927bc787
Embed base nix configuration to binary 2019-12-29 17:41:44 +00:00
Mikhail Klementev 1251a21aba
Allow to change resolution more than 6 times in 10 seconds (default) 2019-12-29 17:24:14 +00:00
Mikhail Klementev e5ccd6d913
Move out xml template 2019-12-29 16:31:22 +00:00
Vladimir Serov 0823b2f028
[fix] starting virtmanager with correct hypervisor uri 2019-12-29 19:08:36 +03:00
Vladimir Serov 4ce9b6ee86
[feat] using remote configs for appvm creation 2019-12-29 18:41:26 +03:00
Vladimir Serov ba569d4aab
[feat] proper config fetching (not yet used) 2019-12-29 18:27:37 +03:00
Vladimir Serov 302d004e85
[feat] added environment files 2019-12-29 18:27:02 +03:00
Mikhail Klementev 55a825a7c0
Generate X monitor section is no more required 2019-12-29 14:36:36 +00:00
Mikhail Klementev 1270b2e209
Temporary solution for automatic resolution updates
Resolves #2
2019-12-29 14:32:41 +00:00
Mikhail Klementev 870c9d6da8
Try copy xmonad configuration again on failure 2019-12-29 13:52:24 +00:00
Mikhail Klementev bd8d6d822d
Change description for xmonad symlink service 2019-12-29 13:27:10 +00:00
Mikhail Klementev 23947390a1
Use full path 2019-12-29 13:15:24 +00:00
Mikhail Klementev 6a54439180
Use go regex
Resolves #9
2019-12-29 07:24:59 +00:00
Mikhail Klementev 959e6e6596
Create appvm directory at start 2019-12-29 07:06:59 +00:00
Mikhail Klementev 8bcf3eae87
Add donate buttons 2019-08-17 15:40:14 +00:00
Mikhail Klementev bb5a352215
Refactor prefix search 2019-05-12 00:17:21 +00:00
Mikhail Klementev 660c8a5dba
Improve error handling 2019-05-12 00:17:20 +00:00
Mikhail Klementev 9ac5d98ddb
Remove state version 2019-05-12 00:17:15 +00:00
Mikhail Klementev 20b9b42e03 Move fake qcow2 image to appvm directory, fixes #10 2019-03-07 11:57:06 +00:00
Mikhail Klementev fd224db22e
Move upstream to code.dumpstack.io 2019-02-03 22:22:47 +00:00
Mikhail Klementev f60945d3c1
Add pull request template 2018-10-07 10:14:15 +00:00
Mikhail Klementev 575e147989
Define torbrowser 2018-08-11 14:55:21 +00:00
Mikhail Klementev ff4e7f8398
Fix typos 2018-08-10 07:16:58 +00:00
Mikhail Klementev 9218a10641
Implements verbose flag 2018-08-04 09:27:53 +00:00
Mikhail Klementev 326b9ce610 Add progress bar 2018-07-15 19:54:38 +00:00
Mikhail Klementev 36267ac5ea Connect to exists VM if already runned 2018-07-15 19:32:28 +00:00
Mikhail Klementev 5bb305a0ec Fix mistake with args 2018-07-15 19:27:34 +00:00
Mikhail Klementev 5f81e7ac3d Move out VM generation 2018-07-15 19:08:28 +00:00
Mikhail Klementev cb343c4506 Move copy templates out 2018-07-15 19:06:16 +00:00
Mikhail Klementev 02f4eab277 Set memory params of memory autoballoning through arguments 2018-07-13 21:31:42 +00:00
44 changed files with 1506 additions and 382 deletions

3
.envrc Normal file
View File

@ -0,0 +1,3 @@
use_nix
export GOPATH=$HOME
export APPVM_CONFIGS=$PWD

4
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View 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.

16
.github/workflows/macos.yml vendored Normal file
View File

@ -0,0 +1,16 @@
name: macOS
on: [push]
jobs:
build:
name: Build
runs-on: macOS-latest
steps:
- uses: actions/checkout@v1
- name: Fetch dependencies
run: go get -d ./...
- name: Build
run: go build

33
.github/workflows/ubuntu.yml vendored Normal file
View 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
View File

@ -1,2 +1 @@
nix/local.nix
nix/monitor.nix
appvm

5
Makefile Normal file
View File

@ -0,0 +1,5 @@
go:
go build
nix:
nix-build #-E '((import <nixpkgs> {}).callPackage (import ./default.nix) { })' --option sandbox true --no-out-link

View File

@ -1,72 +1,47 @@
[![Documentation Status](https://readthedocs.org/projects/appvm/badge/?version=latest)](https://appvm.readthedocs.io/en/latest/?badge=latest)
# Nix application VMs: security through virtualization
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.
Currently optimized for full screen usage (but remote-viewer has ability to resize window dynamically without change resolution).
![appvm screenshot](https://gateway.ipfs.io/ipfs/QmetVp2LRwcy3baxuAjDgBPwv5ych5kRfXeULoNpQAFsaP)
![appvm screenshot](screenshots/2018-07-05.png)
## Installation
## Dependencies
See [related documentation](https://appvm.readthedocs.io/en/latest/installation.html).
$ sudo apt install golang virt-manager curl git
$ sudo usermod -a -G libvirt $USER
## Usage
$ echo 'export GOPATH=$HOME/go' >> ~/.bash_profile
$ echo 'export PATH=$PATH:$GOPATH/bin' >> ~/.bash_profile
$ echo 'source ~/.bash_profile' >> ~/.bashrc
$ source ~/.bash_profile
### Search for applications
You need to **relogin** if you install virt-manager (libvirt) first time.
$ appvm search chromium
## Install Nix package manager
$ 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
### Run application
$ appvm start chromium
$ # ... 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.
## Shared directory
### Shared directory
$ ls appvm/chromium
foo.tar.gz
bar.tar.gz
## Close VM
### Close VM
$ appvm stop chromium
## Automatic ballooning
### Automatic ballooning
Add this command:
@ -76,28 +51,3 @@ to crontab like that:
$ crontab -l
* * * * * /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

494
appvm.go
View File

@ -8,97 +8,36 @@
package main
import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"net"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"github.com/digitalocean/go-libvirt"
"github.com/go-cmd/cmd"
"github.com/jollheef/go-system"
"github.com/olekukonko/tablewriter"
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='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>
`
type networkModel int
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)
}
const (
networkOffline networkModel = iota
networkQemu networkModel = iota
networkLibvirt networkModel = iota
)
func list(l *libvirt.Libvirt) {
domains, err := l.Domains()
@ -108,23 +47,27 @@ func list(l *libvirt.Libvirt) {
fmt.Println("Started VM:")
for _, d := range domains {
if d.Name[0:5] == "appvm" {
if strings.HasPrefix(d.Name, "appvm") {
fmt.Println("\t", d.Name[6:])
}
}
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 {
log.Fatal(err)
}
for _, f := range files {
if f.Name() != "base.nix" &&
f.Name() != "local.nix" && f.Name() != "monitor.nix" &&
f.Name() != "local.nix.template" && f.Name() != "monitor.nix.template" {
fmt.Println("\t", f.Name()[0:len(f.Name())-4])
switch f.Name() {
case "base.nix":
continue
case "local.nix":
continue
}
fmt.Println("\t", f.Name()[0:len(f.Name())-4])
}
}
@ -149,75 +92,196 @@ func copyFile(from, to string) (err error) {
return destination.Close()
}
func start(l *libvirt.Libvirt, name string) {
// Currently binary-only installation is not supported, because we need *.nix configurations
gopath := os.Getenv("GOPATH")
appvmPath := gopath + "/src/github.com/jollheef/appvm"
err := os.Chdir(appvmPath)
if err != nil {
log.Fatal(err)
}
// Copy templates
if _, err := os.Stat(appvmPath + "/nix/local.nix"); os.IsNotExist(err) {
err = copyFile(appvmPath+"/nix/local.nix.template", appvmPath+"/nix/local.nix")
func prepareTemplates(appvmPath string) (err error) {
if _, err = os.Stat(appvmPath + "/nix/local.nix"); os.IsNotExist(err) {
err = ioutil.WriteFile(configDir+"/nix/local.nix", local_nix_template, 0644)
if err != nil {
log.Fatal(err)
return
}
}
if _, err := os.Stat(appvmPath + "/nix/monitor.nix"); os.IsNotExist(err) {
err = copyFile(appvmPath+"/nix/monitor.nix.template", appvmPath+"/nix/monitor.nix")
if err != nil {
log.Fatal(err)
return
}
func streamStdOutErr(command *cmd.Cmd) {
for {
select {
case line := <-command.Stdout:
fmt.Println(line)
case line := <-command.Stderr:
fmt.Fprintln(os.Stderr, line)
}
}
}
stdout, stderr, ret, err := system.System("nix-build", "<nixpkgs/nixos>", "-A", "config.system.build.vm",
"-I", "nixos-config=nix/"+name+".nix", "-I", ".")
if err != nil {
log.Fatalln(err, stdout, stderr, ret)
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)
}
realpath, err := filepath.EvalSymlinks("result/system")
if err != nil {
log.Fatal(err)
status := <-command.Start()
if status.Error != nil || status.Exit != 0 {
log.Println(status.Error, status.Stdout, status.Stderr)
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
}
// TODO: Use go regex
reginfo, _, _, err := system.System("sh", "-c", "cat result/bin/run-nixos-vm | grep -o 'regInfo=.*/registration'")
realpath, err = filepath.EvalSymlinks("result/system")
if err != nil {
log.Fatal(err)
return
}
matches, err := filepath.Glob("result/bin/run-*-vm")
if err != nil || len(matches) != 1 {
return
}
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")
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
qcow2 = os.Getenv("HOME") + "/appvm/." + name + ".fake.qcow2"
if _, e := os.Stat(qcow2); os.IsNotExist(e) {
system.System("qemu-img", "create", "-f", "qcow2", qcow2, "40M")
}
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
}
os.MkdirAll(sharedDir, 0700)
vmName := "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 {
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)
if gui {
cmd := exec.Command("virt-viewer", "-c", "qemu:///system", vmName)
cmd.Start()
}
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)
cmd.Start()
}
func stop(l *libvirt.Libvirt, name string) {
@ -241,36 +305,41 @@ func drop(name string) {
os.RemoveAll(appDataPath)
}
func autoBalloon(l *libvirt.Libvirt) {
func autoBalloon(l *libvirt.Libvirt, memoryMin, adjustPercent uint64) {
domains, err := l.Domains()
if err != nil {
log.Fatal(err)
}
const memoryMin = 512 * 1024 // 512 MiB
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Application VM", "Used memory", "Current memory", "Max memory", "New memory"})
for _, d := range domains {
if d.Name[0:5] == "appvm" {
if strings.HasPrefix(d.Name, "appvm_") {
name := d.Name[6:]
memoryUsedRaw, err := ioutil.ReadFile(os.Getenv("HOME") + "/appvm/" + name + "/.memory_used")
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]))
if err != nil {
log.Fatal(err)
log.Println(err)
continue
}
memoryUsed := memoryUsedMiB * 1024
_, memoryMax, memoryCurrent, _, _, err := l.DomainGetInfo(d)
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 {
memoryNew = memoryMax - 1
@ -282,7 +351,8 @@ func autoBalloon(l *libvirt.Libvirt) {
err = l.DomainSetMemory(d, memoryNew)
if err != nil {
log.Fatal(err)
log.Println(err)
continue
}
table.Append([]string{name,
@ -295,34 +365,172 @@ func autoBalloon(l *libvirt.Libvirt) {
table.Render()
}
func main() {
c, err := net.DialTimeout("unix", "/var/run/libvirt/libvirt-sock", time.Second)
func search(name string) {
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 {
log.Fatal(err)
}
l := libvirt.New(c)
if err := l.Connect(); err != nil {
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)
}
defer l.Disconnect()
kingpin.Command("list", "List applications")
kingpin.Command("autoballoon", "Automatically adjust/reduce app vm memory")
startName := kingpin.Command("start", "Start application").Arg("name", "Application name").Required().String()
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 {
log.Fatal(err)
}
defer l.Disconnect()
cleanupStatelessVMs(l)
}
switch kingpin.Parse() {
case "list":
list(l)
case "search":
search(*searchName)
case "generate":
generate(*generateName, *generateBin, *generateVMName,
*generateBuildVM)
case "start":
start(l, *startName)
networkModel := parseNetworkModel(*startOffline, *startNetwork)
start(l, *startName,
!*startQuiet, networkModel, !*startCli, *startStateless,
*startArgs, *startOpen)
case "stop":
stop(l, *stopName)
case "drop":
drop(*dropName)
case "autoballoon":
autoBalloon(l)
autoBalloon(l, *minMemory*1024, *adjustPercent)
case "sync":
sync()
}
}

122
base.nix.go Normal file
View 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 defaultConfig
{ 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
View 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
View File

@ -0,0 +1,34 @@
{ pkgs ? import <nixpkgs> {}, ... }:
let
virt-manager-without-menu = pkgs.virt-viewer.overrideAttrs(x: {
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 = ./.;
vendorSha256 = "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
View File

@ -0,0 +1,4 @@
Architecture
============
*TODO*

30
docs/index.rst Normal file
View 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

40
docs/installation.rst Normal file
View File

@ -0,0 +1,40 @@
Installation
============
NixOS
-----
First, clone this repo. Then do this:
/etc/nixos/configuration.nix::
imports = [
/path/to/repo/nixos
];
virtualisation.appvm = {
enable = true;
user = "${username}";
};
This is a temporary solution until appvm is upstreamed to nixpkgs or Nix flakes are released.
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
View File

@ -0,0 +1,4 @@
Threat model
=======================
*TODO*

88
dot-desktop-fuse/main.go Normal file
View 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()
}

View File

@ -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
View 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
View 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
View 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
View File

@ -0,0 +1,7 @@
package main
var local_nix_template = []byte(`
{
#services.xserver.xkbOptions = "ctrl:nocaps";
}
`)

View File

@ -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" ];
};
}

View File

@ -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 &";
}

View File

@ -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 &";
}

View File

@ -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 &";
}

View File

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

View File

@ -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"
'';
}

View File

@ -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 &";
}

View File

@ -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 &";
}

View File

@ -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
View 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
View File

@ -0,0 +1,4 @@
*.iso
target.nix
result
nixos.qcow2

20
os/Makefile Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
{
# vm.nix is used for testing only
users.users.root.initialPassword = "root";
virtualisation.memorySize = 8196;
}

View File

@ -0,0 +1,25 @@
From c26580442a4ac18f805b1795ec250d6cf5857a49 Mon Sep 17 00:00:00 2001
From: Mikhail Klementev <blame@dumpstack.io>
Date: Wed, 22 Jan 2020 00:25:16 +0000
Subject: [PATCH 1/4] Remove menu bar
---
src/resources/ui/virt-viewer.ui | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/resources/ui/virt-viewer.ui b/src/resources/ui/virt-viewer.ui
index 93471a4..b029ad7 100644
--- a/src/resources/ui/virt-viewer.ui
+++ b/src/resources/ui/virt-viewer.ui
@@ -18,7 +18,7 @@
<property name="can_focus">False</property>
<child>
<object class="GtkMenuBar" id="top-menu">
- <property name="visible">True</property>
+ <property name="visible">False</property>
<property name="can_focus">False</property>
<child>
<object class="GtkMenuItem" id="menu-file">
--
2.23.1

View File

@ -0,0 +1,27 @@
From eac430d01f486e15b55d6fa992e77ded77bb4b1a Mon Sep 17 00:00:00 2001
From: Mikhail Klementev <blame@dumpstack.io>
Date: Wed, 22 Jan 2020 00:41:34 +0000
Subject: [PATCH 2/4] Do not grab keyboard/mouse
---
src/virt-viewer-display-spice.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/virt-viewer-display-spice.c b/src/virt-viewer-display-spice.c
index 311178b..8e68de2 100644
--- a/src/virt-viewer-display-spice.c
+++ b/src/virt-viewer-display-spice.c
@@ -307,8 +307,8 @@ virt_viewer_display_spice_new(VirtViewerSessionSpice *session,
gtk_container_add(GTK_CONTAINER(self), GTK_WIDGET(self->priv->display));
gtk_widget_show(GTK_WIDGET(self->priv->display));
g_object_set(self->priv->display,
- "grab-keyboard", TRUE,
- "grab-mouse", TRUE,
+ "grab-keyboard", FALSE,
+ "grab-mouse", FALSE,
"resize-guest", FALSE,
"scaling", TRUE,
NULL);
--
2.23.1

View File

@ -0,0 +1,26 @@
From c5b51c0a540af34499f2b2a49cb64599c06ba293 Mon Sep 17 00:00:00 2001
From: Mikhail Klementev <blame@dumpstack.io>
Date: Wed, 22 Jan 2020 01:24:57 +0000
Subject: [PATCH 3/4] Use name of appvm applications as a title
---
src/virt-viewer-window.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/virt-viewer-window.c b/src/virt-viewer-window.c
index 4c08423..cc8a440 100644
--- a/src/virt-viewer-window.c
+++ b/src/virt-viewer-window.c
@@ -1362,6 +1362,9 @@ virt_viewer_window_update_title(VirtViewerWindow *self)
if (!ungrab && !priv->subtitle)
title = g_strdup(g_get_application_name());
+ else if (g_str_has_prefix(priv->subtitle, "appvm_"))
+ /* Use name of the application as a title */
+ title = g_strdup_printf(_("%s"), &priv->subtitle[strlen("appvm_")]);
else
/* translators:
* This is "<ungrab (or empty)><space (or empty)><subtitle (or empty)> - <appname>"
--
2.23.1

View File

@ -0,0 +1,44 @@
From 8e95408365b57c64a738381d132ecdc844013afb Mon Sep 17 00:00:00 2001
From: Mikhail Klementev <blame@dumpstack.io>
Date: Wed, 22 Jan 2020 02:27:26 +0000
Subject: [PATCH 4/4] Use title (application name) as subtitle
---
src/virt-viewer-app.c | 21 ++-------------------
1 file changed, 2 insertions(+), 19 deletions(-)
diff --git a/src/virt-viewer-app.c b/src/virt-viewer-app.c
index 343b1af..8267f82 100644
--- a/src/virt-viewer-app.c
+++ b/src/virt-viewer-app.c
@@ -718,25 +718,8 @@ virt_viewer_app_set_window_subtitle(VirtViewerApp *app,
gchar *subtitle = NULL;
const gchar *title = virt_viewer_app_get_title(app);
- if (title != NULL) {
- VirtViewerDisplay *display = virt_viewer_window_get_display(window);
- gchar *d = strstr(title, "%d");
- gchar *desc = NULL;
-
- if (display && VIRT_VIEWER_IS_DISPLAY_VTE(display)) {
- g_object_get(display, "name", &desc, NULL);
- } else {
- desc = g_strdup_printf("%d", nth + 1);
- }
-
- if (d != NULL) {
- *d = '\0';
- subtitle = g_strdup_printf("%s%s%s", title, desc, d + 2);
- *d = '%';
- } else
- subtitle = g_strdup_printf("%s (%s)", title, desc);
- g_free(desc);
- }
+ if (title != NULL)
+ subtitle = g_strdup_printf("%s", title);
g_object_set(window, "subtitle", subtitle, NULL);
g_free(subtitle);
--
2.23.1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 KiB

4
shell.nix Normal file
View File

@ -0,0 +1,4 @@
{ pkgs ? import <nixpkgs> {} }:
with pkgs; mkShell {
buildInputs = [ go gocode virt-viewer virtmanager ];
}

115
xml.go Normal file
View 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>
`