Really disposable (RAM based) qubes

Hi,

While researching, I found @unman’s GitHub repo and another user’s improvement of the original bash script. After noticing some issues (with log paths), I allowed myself the liberty to fix them and add further flexibility to the script. One essential new thing is that no log files are saved on the file system at all as they exist only as symlinks to /dev/null. Additionally, now names are not fixed and one can use the script based on one’s own templates and preferences.

I still don’t know how to work with git, so I will just share my version here in case someone finds it useful:

#!/bin/bash
#
# Create, launch, and clean up a RAM based disposable qube in Qubes OS's dom0
#
# Inspired by:
# https://github.com/unman/notes/Really_Disposable_Qubes.md
# https://github.com/kennethrrosen/qubes-shadow-dvm/

set -euo pipefail

# Display error message and notification
error()
{
	local -a args=('-e')
	local -r light_red='\033[1;31m'
	local -r nocolor='\033[0m'
	args+=("${@}")
	args[1]="${light_red}${args[1]}${nocolor}"
	echo "${args[@]}" 1>&2

	notify-send --expire-time 5000 \
		    --icon='/usr/share/icons/Adwaita/256x256/legacy/dialog-error.png' \
		    "${0##*/}" \
		    "${*}"
}

# Prevent two processes from trying to create the same qube
readonly pidfile="/run/user/${UID}/${0##*/}.pid"
if [ -f "${pidfile}" ]; then
	error "Another ${0##*/} instance is currently running."
	exit 1
fi


if [ $# -eq 0 ]; then
	cat >&2 <<-EOF
	Usage: ${0##*/} [options] -c <command>
	 -c, --command             Command to execute inside the Qube
	Optional [defaults]:
	 -q, --qubename            Qube name [rdispN], where N is 100-9999
	 -d, --tempdir             Mountpoint for the RAM drive
	                             [${HOME}/tmp/<qubename>]
	 -s, --tempsize            RAM drive size (1G, 2G ...) [1G]
	 -p, --property name=value Sets domain's properties.
	                           If not set, these defaults are used:
	                             include_in_backups=false
	                             netvm= (i.e. none)
	                             memory=1000
	                             template_for_dispvms=false
	                             default_dispvm= (i.e. none)
	                             label - based on netvm:
	                               - netvm= (i.e .none) => gray
	                               - netvm=sys-whonix => purple
	                               - netvm=<any other> => red
	                           Use label after netvm to override.
	                           Last set values override previous ones.
	                           See man qvm-prefs for all properties.

	EXAMPLE: Launch Tor browser in a RAM based whonix disposable:
	 ${0##*/} -p template=whonix-ws-16-dvm -p netvm=sys-whonix -c torbrowser
	EOF
	exit 1
fi

tempdir_root="${HOME}/tmp"
tempsize='1G'
properties=('include_in_backups=false'
	    'template_for_dispvms=false'
	    'netvm='
	    'memory=1000'
	    'default_dispvm='
	    'label=gray')
# Generate Qubes OS style random name: rdisp100-rdisp9999
# making sure it does not duplicate existing VM name
while : ; do
	qube_name=$(/usr/bin/shuf --input-range=100-9999 --head-count=1)
	qube_name="rdisp${qube_name}"
	if ! qvm-check "${qube_name}" > /dev/null 2>&1; then
		break
	fi
done

set +u
while : ; do
	case "${1}" in
		-q | --qubename)
			if qvm-check "${2}" > /dev/null 2>&1; then
				error "${2}" "already exists. Exiting."
				exit 1
			fi
			qube_name="${2}"
                        shift 2
                        ;;
		-c | --command)
                        command_to_run="${2}"
                        shift 2
                        ;;
		-d | --tempdir)
			if [ -d "${2}" ]; then
				error "${2}:" 'The directory exists'
				exit 1
			fi
			tempdir="${2}"
			shift 2
			;;
		-s | --tempsize)
			# TODO: Validate size value
			# Show error message if size exceeds
			# available memory
			tempsize="${2}"
			shift 2
			;;
		-p | --property)
			if ! grep -qiE '^[^=]+=[^=]*$' <<< "${2}"; then
				error 'Wrong property pattern:' \
					'Usage: --property name=value'
				exit 1
			fi
			if [[ "${2}" == 'netvm=sys-whonix' ]]; then
				properties+=( 'label=purple' )
			elif [[ "${2}" =~ netvm=.+ ]]; then
				properties+=( 'label=red' )
			fi
			if [[ "${2}" =~ template=.+ ]]; then
				template=$(echo "${2}" \
					        | sed -r 's/^template=//g')
				shift 2
				continue
			fi
			properties+=( "${2}" )
			shift 2
			;;
		--) # End of all options
			shift
			break;
			;;
		-*)
			error 'Unknown option:' "${1}"
			exit 1
			;;
		*)  # No more options
			break
			;;
	esac
done

cleanup()
{
	local exit_code="${1}"
	set +e
	qvm-kill "${qube_name}"
	qvm-remove --force "${qube_name}"
	qvm-pool remove "${pool_name}"
	sudo umount "${pool_name}"

	# Leave no trace on file system
	find "${HOME}/.config/menus/applications-merged" \
		-regextype posix-egrep \
		-regex \
		".*\/user-qubes-(disp)?vm-directory(_|-)${qube_name}\.menu$" \
		-delete
	sudo rm -rf "${tempdir}" \
		"/run/qubes/audio-control.${qube_name}"
	for file in "${logfiles[@]}"; do
		sudo rm -rf "${file}" "${file}.old"
	done
	# Remove the root of temp directories
	rmdir --ignore-fail-on-non-empty "${tempdir_root}"
	notify-send --expire-time 5000 \
		    --icon='/usr/share/icons/Adwaita/scalable/emblems/emblem-default-symbolic.svg' \
		    "${qube_name}" \
		    "Remnants cleared"
	rm -f "${pidfile}"
	exit "${exit_code}"
}

if [[ $(qvm-prefs "${template}" template_for_dispvms) != True ]]; then
	error "${template}" 'is not a disposable template'
	cleanup 1 > /dev/null 2>&1;
fi

set -u
notify-send --expire-time 10000 \
	     --icon='/usr/share/icons/hicolor/scalable/apps/xfce4-timer-plugin.svg' \
	     "${0##*/}" \
	     "Attempting to create ${qube_name}"

tempdir="${tempdir_root}/${qube_name}"
if [ -d "${tempdir}" ]; then
	error "${tempdir}" 'already exists. Exiting.'
	exit 1
fi
pool_name="ram_pool_${qube_name}"
if qvm-pool info "${pool_name}" > /dev/null 2>&1; then
	error "${pool_name}" 'already exists. Exiting.'
	exit 1
fi

logdir='/var/log'
logfiles=("${logdir}/libvirt/libxl/${qube_name}.log"
	  "${logdir}/qubes/guid.${qube_name}.log"
	  "${logdir}/qubes/qrexec.${qube_name}.log"
	  "${logdir}/qubes/qubesdb.${qube_name}.log"
	  "${logdir}/xen/console/guest-${qube_name}.log")

main()
{
	sudo swapoff --all
	mkdir --parents "${tempdir}"

	sudo mount --types tmpfs \
		   --options size="${tempsize}" \
		   "${pool_name}" \
		   "${tempdir}"
	qvm-pool add "${pool_name}" \
		 file \
		 --option revisions_to_keep=1 \
		 --option dir_path="${tempdir}" \
		 --option ephemeral_volatile=True

	# Create void symlinks to prevent log saving
	for file in "${logfiles[@]}"; do
		sudo ln -sfT /dev/null "${file}"
	done

	qvm-clone  --quiet -P "${pool_name}" "${template}" "${qube_name}" \
		   || cleanup 1
	qvm-volume config "${qube_name}:root" rw False
	local property
	local prop_name
	local prop_value
	for property in "${properties[@]}"; do
		prop_name=$(echo "${property}" \
			         | sed -r 's/(^--property=)//g' \
				 | sed -r 's/=[^=]*$//g')
		prop_value=$(echo "${property}" \
                                 | sed -r 's/(^--property=)//g' \
				 | sed -r 's/[^=]+=//g')
		qvm-prefs "${qube_name}" "${prop_name}" "${prop_value}" \
			  || cleanup 1
	done
	unset property prop_name prop_value
	# Process locking is necessary only during qube creation
	rm -f "${pidfile}"
	set +e
	qvm-run "${qube_name}" "${command_to_run}"
	set -e
	cleanup 0
}

touch "${pidfile}"
trap 'cleanup' SIGINT SIGTERM

main "${@}"

For example usage start the script in console without arguments.

Here is also a second script which cleans up remnants of ANY qubes (not only those created by the script, i.e. RAM based, but also “traditional” ones). Some users observed that a system shutdown does not allow the script from above to complete, so remnants need to be cleaned manually (now with the help of this script). This second script is also a workaround for this issue. Use it with caution!

#!/bin/bash
#
# Remove RAM pools, logs and menu files for non-existing qubes

set -euo pipefail

readonly light_green='\033[1;32m'
readonly yellow='\033[1;33m'
readonly light_blue='\033[1;34m'
readonly light_purple='\033[1;35m'
readonly light_cyan='\033[1;36m'
readonly white='\033[1;37m'
readonly no_color='\033[0m'
readonly bg_black='\033[40m'
readonly bg_red='\033[41m'

readonly logdir='/var/log'
readonly tempdir_root="${HOME}/tmp"
readonly menudir="${HOME}/.config/menus/applications-merged"

pause()
{
	local answer
	local message
	message+="${bg_red}${white}Yes${no_color}/"
	message+="${bg_black}${yellow}No${no_color}/"
	message+="${light_green}Quit${no_color}? "
	while true; do
		read -r -n 1 -p "$(echo -e "${message}")" answer
		case "${answer}" in
			[Yy]* ) echo; break;;
			[Nn]* ) echo; return 1;;
			[Qq]* ) echo -e "\nExitting. Bye.\n"; exit;;
			* ) echo -e "\nPlease answer (y)es, (n)o or (q)uit.";;
		esac
	done
}

notice()
{
	printf "${light_blue}%s${no_color}\n" "${@}"
}

remove_unused_pools()
{
	[ -z "${1}" ] && return
	local pools_list="${1}"
	readarray -t pools_list < <(echo "${pools_list}")
	for pool_name in "${pools_list[@]}"; do
		qube_name=$(echo "${pool_name}" | sed -r 's/^ram_pool_//')
		if qvm-check "${qube_name}" > /dev/null 2>&1; then
			# The qube exists
			continue
		fi
		local pool_mountpoint
		pool_mountpoint=$(qvm-pool info "${pool_name}" \
			                   | grep -E '^dir_path' \
					   | sed -r 's/^dir_path\s+//g')

		printf "%s ${light_cyan}%s${no_color} " \
			"Remove pool" "${pool_name}"
		printf "%s ${light_cyan}%s${no_color}: " \
			"with dir_path" "${pool_mountpoint}"
		! pause && continue
		qvm-pool remove "${pool_name}"
		set +e
		sudo umount "${pool_mountpoint}"
		set -e
		sudo rm -rf "${pool_mountpoint}"
	done
}

remove_qubes_files()
{
	[ -z "${1}" ] && return
	local qubes_list="${1}"
	readarray -t qubes_list < <(echo "${qubes_list}")
	local qube_name
	local decoded_qube_name

	for qube_name in "${qubes_list[@]}"; do
		decoded_qube_name=$(echo "${qube_name}" \
			| sed -r 's/_d/-/g' \
			| sed -r 's/_u/_/g'
		)
		if qvm-check "${decoded_qube_name}" > /dev/null 2>&1; then
			# The qube exists
			continue
		fi
		printf "\n%s ${light_purple}%s${no_color} %s\n" \
			"Qube" "${decoded_qube_name}" "does not exist"
		#readarray -d '' files < <(find "${dir}" -name "$input" -print0)
		# NOTE: It's good that qube names are simple
		# and won't need regex escaping
		local log_pattern="${qube_name}\.log((\.old)|(-[0-9]{8}))?(\.gz)?"
		local menu_pattern="user-qubes-(disp)?vm-directory(_|-)${qube_name}\.menu"

		local -A targets
		targets=(["${logdir}/libvirt/libxl"]="${log_pattern}"
			 ["${logdir}/qubes"]="((guid|qrexec|qubesdb)\.)?${log_pattern}"
			 ["${logdir}/xen/console"]="guest-${log_pattern}"
			 ["${menudir}"]="${menu_pattern}")
		if [ -d "${tempdir_root}" ]; then
			targets+=(["${tempdir_root}"]="${qube_name}")
		fi
		local remnant
		local search_dir
		local -a remnants=()
		for search_dir in "${!targets[@]}"; do
			local intermediate_results
			mapfile -d $'\0' intermediate_results \
				< <(sudo find "${search_dir}" \
				         -regextype posix-egrep \
					 -regex ".*\/${targets[${search_dir}]}$" \
					 -print0)
			remnants+=("${intermediate_results[@]}")
			unset intermediate_results
		done
		if [[ "${#remnants[@]}" == 0 ]]; then
			echo 'No files found'
			continue
		fi
		for remnant in "${remnants[@]}"; do
			local message="Delete ${light_cyan}${remnant}${no_color}: "
			message=$(echo -e "${message}" \
				       | sed -r "s/${qube_name}/\\${light_purple}${qube_name}\\${light_cyan}/g")
			echo -ne "${message}"
			! pause && continue
			sudo rm -rf "${remnant}"
		done
	done
}

warning=$(cat <<-EOF
WARNING!!!
This script searches for remnants of ANY non-existing qubes.
You will be asked to confirm each change individually.
Be careful and think twice before confirming anything!
You have been warned.
EOF
)
readonly warning

printf "${bg_red}${white}%s${no_color}\n" "${warning}"
echo 'If there are no remnants, you will not have to do anything.'
echo 'Continue?'
pause || exit 1

existing_qubes=$(qvm-ls --fields=name \
	                --raw-data \
			| sort)
readonly existing_qubes

# Look for qubes in the logs
all_qube_names=$(sudo find "${logdir}/qubes/" \
	                   "${logdir}/libvirt/libxl/" \
			   -type f \
			   -regextype posix-egrep \
			   -regex '.*\.log((\.old)|(-[0-9]{8}))?(\.gz)?$' \
			   -exec basename "{}" \; \
			   | sed -r 's/\.log((\.old)|(-[0-9]{8}))?(\.gz)?$//g' \
			   | sed -r 's/^(guid|qrexec|qubesdb)\.//g' \
			   | sort \
			   | uniq)$'\n'
all_qube_names+=$(sudo find "${logdir}/xen/console/" \
	                    -type f \
			    -regextype posix-egrep \
			    -regex '.*\/guest-.*\.log((\.old)|(-[0-9]{8}))?(\.gz)?$' \
			    -exec basename "{}" \; \
			    | sed -r 's/\.log((\.old)|(-[0-9]{8}))?(\.gz)?$//g' \
			    | sed -r 's/^guest-//g' \
			    | sort \
			    | uniq)$'\n'

# Look for qubes in the RAM pools
set +e
ram_pools=$(qvm-pool list \
	             | grep -Eio '^ram_pool_[^ ]+' \
		     | sort \
		     | uniq)
set -e
all_qube_names+=$(echo "${ram_pools}" | sed -r 's/^ram_pool_//g')$'\n'

# Look for qubes in mountpoints
if [ -d "${tempdir_root}" ]; then
	all_qube_names+=$(find "${tempdir_root}" \
		               -mindepth 1 \
			       -maxdepth 1 \
			       -type d \
			       -exec basename "{}" \; \
			       | sort \
			       | uniq)$'\n'
fi

# Look for qubes in menus directory
all_qube_names+=$(find "${menudir}" \
	               -regextype posix-egrep \
		       -regex '.*\/user-qubes-.*\.menu$' \
		       -exec basename "{}" \; \
		       | sed -r 's/\.menu$//g' \
		       | sed -r 's/^user-qubes-(disp)?vm-directory(_|-)//g' \
		       | sort \
		       | uniq)$'\n'

all_qube_names=$(echo "${all_qube_names}" \
	              | sed -r 's/^(Domain-0|libxl-driver)$//g' \
		      | sed -r '/^\s*$/d' \
		      | sort \
		      | uniq)
set +e
qubes_to_remove=$(diff --new-line-format='' \
	               --unchanged-line-format='' \
		       <(echo "${all_qube_names}") \
		       <(echo "${existing_qubes}") \
		       | sed -r '/^\s*$/d')
set -e

notice 'Checking for unused RAM qube pools'
remove_unused_pools "${ram_pools}"
notice 'Finished checking for unused RAM qube pools'

set +e
qubes_to_remove=$(echo "${qubes_to_remove}" | sed -r '/^\s*$/d')
set -e
notice 'Checking for files with no corresponding qubes'
remove_qubes_files "${qubes_to_remove}"
notice 'Finished checking for qube file remnants'

if [[ -d "${tempdir_root}" && -z "$(ls -A "${tempdir_root}")" ]]; then
	printf "%s ${light_cyan}%s${no_color}: " 'Delete' "${tempdir_root}"
	pause && rmdir "${tempdir_root}"
fi
notice 'Done'

One more script to monitor RAM pool and volume usage. Could be useful for optimizing memory related qube settings (e.g. run watch -c pool-usage in a dom0 console while working with various qubes):

#!/bin/bash

set -euo pipefail
/usr/bin/renice -n 19 $$ > /dev/null 2>&1

readonly light_green='\033[1;32m'
readonly light_purple='\033[1;35m'
readonly white='\033[1;37m'
readonly no_color='\033[0m'
readonly bg_red='\033[41m'

echo -ne "volatile volume: ${white}${bg_red}non ephemeral${no_color}"
echo -e "${light_green} ephemeral${no_color}"

output()
{
	local text="${1}"
	local ephemeral="${2}"
	local size="${3}"
	local usage="${4}"
	local color="${white}${bg_red}"
	[[ "${ephemeral}" =~ 'True' ]] && color="${light_green}"

	local percent
	local limit=80
	percent=$(awk -v usage="${usage}" -v size="${size}" \
		       'BEGIN {printf "%3.2f", 100*usage/size}')
	local percent_color="${no_color}"
	if [ "${percent%.*}" -gt "${limit%.*}" ]; then
		percent_color="${white}${bg_red}"
	fi
	local size_gb
	size_gb=$(awk -v size="${size}" \
		       'BEGIN {printf "%2.2f", size/1024/1024/1024}')
	printf "${color}%-18s${no_color} " "${text}"
	printf "${percent_color}%6.2f%%${no_color} of %5.2f GiB\n" \
		"${percent}" "${size_gb}"
}

main()
{
	local ram_pools
	mapfile -t ram_pools < <(qvm-pool list \
		                 | grep -Eo '^ram_pool_[^ ]+')
	for pool in "${ram_pools[@]}"; do
		local qube
		qube="${pool#ram_pool_}"
		printf "${light_purple}%s${no_color}\n" "${qube}"
		local ephemeral_volatile=''
		ephemeral_volatile=$(qvm-pool info "${pool}" \
			                      | grep -E '^ephemeral_volatile')
		local size
		size=$(qvm-pool info "${pool}" \
			        | grep -E '^size' \
			        | grep -Eo '[0-9]+')
		local usage
		usage=$(qvm-pool info "${pool}" \
			         | grep -E '^usage' \
				 | grep -Eo '[0-9]+')
		output "${pool}" "${ephemeral_volatile}" "${size}" "${usage}"

		local volume
		for volume in 'volatile' 'private'; do
			local ephemeral=''
			ephemeral=$(qvm-volume info "${qube}:${volume}" ephemeral)
			size=$(qvm-volume info "${qube}:${volume}" size)
			usage=$(qvm-volume info "${qube}:${volume}" usage)
			output "${volume}" "${ephemeral}" "${size}" "${usage}"
		done
	done
}

main "${@}"

Your comments and suggestions are welcome.


Edits:

  • @barto’s fixes
  • added -l, --label option
  • fix whitespaces
  • added -k, --kernel and -e, --ephemeral options, removed unnecessary wait. Now even forcefully killed qubes should get proper cleanup.
  • removed ephemeral until there is more clarity about it
  • reworked to allow all properties supported by qvm-create
  • added a second script for easy cleanup (when necessary)
  • script 1: improved checks, now pool names match custom qube name; script 2: find more logs, added qube name colorization in log file paths
  • New feature: automatic label based on netvm value (custom label is still possible); added process locking to prevent 2+ instances trying to create the same qube.
  • fixed a bug with auto labeling. It works as expected now.
  • After clarifications on GitHub and here, reworked to use AppVMs instead. The DVM template is copied to RAM and runs from there.
  • made AppVM’s root volume read-only
  • added pool-usage script
  • fixed a typo bug in the cleanup script
  • added icons to notifications; decreased default tempsize to 1 GiB
  • updated filename patterns for ram-qube and remove-qube-remnants script to clean up correctly in Qubes OS 4.2.0
13 Likes
Moved to Community Guides

Since this is not a question looking for an answer, I moved it to the Community Guides category. Please feel free to change that as you see fit @qubist !

2 Likes

The link from @Tezeria might be of interest

Encryption solves your problem. I saw somewhere that starting from 4.1 you can encrypt with random your disp vms

Thanks for the improvements! My take on @unman’s was a modest attempt. This is great and happy to see “shadow” brought over :grinning:. Also worth including here:

1 Like

when I ran sudo bash text.sh shadow-qube -t whonix-ws-16-dvm torbrowser -n sys-whonix I get text.sh: line 146: template: unbound variable, I’m on Q:OS 4.2.

Thanks for the effort @qubist , much appreciated!

Noticed that the logic is wrong in the temp directory detection:

This code complains that the directory exists when it doesn’t exist! … so remove the “!” in the test.

Another observation is also about the temp dir, this time the default one. If the user launches more than one “volatile”, the first one to be terminated will delete (rm -rf) the common temp directory, clobbering the remaining instances.
The solution to this is to move the initial assignment for tempdir AFTER the qube_name assignment, and change it to

tempdir="${HOME}/tmp${qube_name}"
1 Like

@procShield
@kenosen

Thank you. It is of interest. I will need some time to understand what exactly all this means.

@dalaylapka

Encryption solves your problem.

Which problem?

@duggy

The script should be run as user, not as root. I am using Qubes OS 4.1.

I don’t know why you are getting the unbound variable message. All variables on that line are defined and used before that line. Do you have the templates from the command installed?

@barto

Thank you. I will fix that.

Actually, I think the rm -rf won’t delete it but would rather fail because the mount point would be in use. Anyway, I will fix it.

2 Likes

First, what a good job to @qubist (and of course to everyone who participates) ! :wink:

I’ve seen this but, apart from the “perfection” (and I know this is really important here) what is the impact of this “bug”?

Yes, it happens, but not every time and the folder remains empty anyway, so another question; Is it so disturbing to have an empty folder?

Two questions purely for my “general knowledge”, in any case, really a great job that deserves to be part of the 4.2!! :slight_smile:

After that 2 questions, the last one: the dispVMs are all in red, is there a parameter to change the color?

@Tezeria

The bug is already fixed in the edited initial post. There should be no empty folders or any leftovers after successful termination of the disposable qubes. Each qube has its unique temp dir now.

After that 2 questions, the last one: the dispVMs are all in red, is there a parameter to change the color?

OK, adding that too. Use option -l (or --label).

1 Like

Damn it! I should have thought a little bit before asking this question! lol. thank’s @qubist :slight_smile:

@Tezeria

Don’t blame yourself. I added the option in the OP after your post :slight_smile:

Ideally, we should have all the options of qvm-prefs. I am questioning, though, if this is the direction this script should take because, ideally, we should simply have a checkbox “Run entirely in RAM” in standard settings UI. I don’t know how to do that though, so I hope Qubes OS’s developers will.

@qubist

Sorry if the questions sounds a bit dumb but how do I kill the qube so that everything is deleted?
I’ve used the shadow qube previously that you refrenced and there was a seperate script to do that.

with the last script i have this error:
line 183: warning :<< here-documents >> on line 29 delimited by the end of the file (instead of “EOF”)

I tried to compare with the latest script that works but I don’t see where the error can come from :confused:
this one is ok:

#!/bin/bash
#
# Create, launch, and clean up a truly disposable qube in Qubes OS's dom0
#
# Inspired by:
# https://github.com/unman/notes/Really_Disposable_Qubes.md
# https://github.com/kennethrrosen/qubes-shadow-dvm/

set -euo pipefail

# Colorize first argument of output text
error()
{
	local command='/usr/bin/echo'
	local -a args=('-e')

	local -r light_red='\033[1;31m'
	local -r nocolor='\033[0m'
	args+=("${@}")
	args[1]="${light_red}${args[1]}${nocolor}"
	"${command}" "${args[@]}" 1>&2
}

if [ $# -eq 0 ]; then
	cat >&2 <<-EOF
	Usage: ${0##*/} [options] -t <template> -c <command>
	 -t, --template       Template VM
	 -c, --command        Command to execute inside the Qube
	Optional [defaults]:
	 -q, --qubename       Qube name [disp100-disp9999]
	 -n, --netvm          NetVM for the qube [none]
	 -d, --tempdir        Mountpoint for the RAM drive [${HOME}/tmp]
	 -s, --tempsize       RAM drive size (1G, 2G ...) [2G]
	 -m, --memory         Qube memory in MB [1000]
	 -v, --default_dispvm Default disposable template [none]
	EOF
	exit 1
fi

netvm=''
tempsize='2G'
memory='1000'
default_dispvm=''
# Generate Qubes OS style random name: disp100-disp9999
# making sure it does not duplicate existing VM name
while : ; do
	qube_name=$(/usr/bin/shuf --input-range=100-9999 --head-count=1)
	tempdir="${HOME}/tmp${qube_name}"
	qube_name="disp${qube_name}"
	pool_name="ram_pool_${qube_name}"
	if ! qvm-check "${qube_name}" > /dev/null 2>&1; then
		break
	fi
done

set +u
while : ; do
	case "${1}" in
		-q | --qubename)
			if qvm-check "${2}" > /dev/null 2>&1; then
				error "${2}:" "already exists. Exiting."
				exit 1
			fi
			qube_name="${2}"
                        shift 2
                        ;;
		-t | --template)
                        # TODO: Show error message if a template
			# does not exist
                        template="${2}"
                        shift 2
                        ;;
		-c | --command)
                        command_to_run="${2}"
                        shift 2
                        ;;
                -n | --netvm)
                        netvm="${2}"
                        shift 2
                        ;;
		-d | --tempdir)
			if [ -d "${2}" ]; then
				error "${2}:" 'Directory exists'
				exit 1
			fi
			tempdir="${2}"
			shift 2
			;;
		-s | --tempsize)
			# TODO: Validate size value
			# Show error message if size exceeds
			# available memory
			tempsize="${2}"
			shift 2
			;;
		-m | --memory)
			# TODO: Validate memory value
			# Show error message if memory exceeds
			# available memory or tempsize
			memory="${2}"
			shift 2
			;;
                -v | --default_dispvm)
                        # TODO: Show error message if a template
                        # does not exist
                        default_dispvm="${2}"
                        shift 2
                        ;;
		--) # End of all options
			shift
			break;
			;;
		-*)
			error 'Unknown option:' "${1}"
			exit 1
			;;
		*)  # No more options
			break
			;;
	esac
done
set -u
sudo swapoff --all
mkdir --parents "${tempdir}"

sudo mount --types tmpfs \
	   --options size="${tempsize}" \
	   "${pool_name}" \
	   "${tempdir}"
qvm-pool add "${pool_name}" \
	 file \
	 --option revisions_to_keep=1 \
	 --option dir_path="${tempdir}"

# Create void symlinks to prevent log saving
logdir='/var/log'
logfiles=("${logdir}/libvirt/libxl/${qube_name}.log"
	  "${logdir}/qubes/guid.${qube_name}.log"
	  "${logdir}/qubes/qrexec.${qube_name}.log"
	  "${logdir}/qubes/qubesdb.${qube_name}.log"
	  "${logdir}/xen/console/guest-${qube_name}.log")
for file in "${logfiles[@]}"; do
	sudo ln -sfT /dev/null "${file}"
done

qvm-create --class DispVM \
	   "${qube_name}" \
	   -P "${pool_name}" \
	   --template="${template}" \
	   --label=red \
	   --property netvm="${netvm}" \
	   --property memory="${memory}" \
	   --property default_dispvm="${default_dispvm}"
qvm-run "${qube_name}" "${command_to_run}"
wait

qvm-kill "${qube_name}"
qvm-remove --force "${qube_name}"
qvm-pool remove "${pool_name}"
sudo umount "${pool_name}"

# Leave no trace on file system
sudo rm -rf "${tempdir}"
for file in "${logfiles[@]}"; do
        sudo rm -rf "${file}" "${file}.old"
done

notify-send --expire-time 5000 \
	    "${qube_name} qube" \
	    "${qube_name} qube remnants cleared."

But this one and and the following show the same error.
is it just me?

PS: i’m always on 4.1.2

@w4rdl0rd

This script handles clean up automatically once the qube shuts down. If you kill the qube forcefully while it is running, this will terminate the script execution and everything after the wait command won’t run, including cleanup.

I have not run the referenced scripts, just used the second one as a starting point. If you have leftovers from qubes created by other scripts, you should clean up manually. This script cannot remove those other qubes.

@qubist
Thanks for clearing up my question.

Was just a bit confused since I tried first forcefully quitting the qube and when this didn’t work shutting down another one.
It wasn’t running as expected since the qubes were still in the Qube Manager list. The referenced shadow script removed it and even though the part of the script has the same function it was still there.

Wrong version deleted…

@Tezeria

Your pasted version seems to be based on an earlier edit.

Re. heredoc error: It seems something messed up the whitespaces while pasting. Should be fixed now (use the latest edit of the OP).

As for color - I don’t know what you mean. The label is an option, not mandatory. The default value is red.

Let me know if everything works now.

Indeed, the version I pasted was an earlier version, the following ones didn’t work at all for me
(an error on the line: cat >&2 <<-EOF) so I had based myself on the last version that worked at home… I’m not an expert in programming and I tried to do my best :confused: .In the one I edited, it worked except for the fact that I had to put a label. Your latest version works PERFECTLY, with or without a label :slight_smile: The only thing is that the DispVMs folders are stored directly in the HOME folder and not in the.tmp. I delete “my version”. I just wanted to try to help :confused:

#!/bin/bash
#
# Create, launch, and clean up a truly disposable qube in Qubes OS's dom0
#
# Inspired by:
# https://github.com/unman/notes/Really_Disposable_Qubes.md
# https://github.com/kennethrrosen/qubes-shadow-dvm/

set -euo pipefail

# Colorize first argument of output text
error()
{
        local command='/usr/bin/echo'
        local -a args=('-e')

        local -r light_red='\033[1;31m'
        local -r nocolor='\033[0m'
        args+=("${@}")
        args[1]="${light_red}${args[1]}${nocolor}"
        "${command}" "${args[@]}" 1>&2

        notify-send --expire-time 5000 \
                    "${0##*/}" \
                    "${*}"
}

if [ $# -eq 0 ]; then
        cat >&2 <<-EOF
        Usage: ${0##*/} [options] -t <template> -c <command>
         -t, --template       Template VM
         -c, --command        Command to execute inside the Qube
        Optional [defaults]:
         -q, --qubename       Qube name [dispN], where N is 100-9999
         -n, --netvm          NetVM for the qube [none]
         -d, --tempdir        Mountpoint for the RAM drive full path [/tmp/shadow/dispN]
         -s, --tempsize       RAM drive size (1G, 2G ...) [2G]
         -m, --memory         Qube memory in MB [1000]
         -v, --default_dispvm Default disposable template [none]
         -k, --kernel         Default Kernel [qubes-prefs default_kernel]
         -l, --label          Label for the domain (red, yellow, ...) [red]
         -e, --ephemeral      Ephemeral qube [false]
EOF
        exit 1
fi

ephemeral='false'
label='red'
netvm=''
tempsize='2G'
memory='2000'
default_dispvm=''
kernel="$(qubes-prefs default_kernel)"
# Generate Qubes OS style random name: disp100-disp9999
# making sure it does not duplicate existing VM name
while : ; do
        qube_name=$(/usr/bin/shuf --input-range=100-9999 --head-count=1)
        qube_name="disp${qube_name}"
        tempdir="/tmp/shadow-qube/tmp_${qube_name}"
        [ -d "${tempdir}" ] && continue
        pool_name="ram_pool_${qube_name}"
        if ! qvm-check "${qube_name}" > /dev/null 2>&1; then
                break
        fi
done

set +u
while : ; do
        case "${1}" in
                -q | --qubename)
                        if qvm-check "${2}" > /dev/null 2>&1; then
                                error "${2}:" "already exists. Exiting."
                                exit 1
                        fi
                        qube_name="${2}"
                        shift 2
                        ;;
                -t | --template)
                        # TODO: Show error message if a template
                        # does not exist
                        template="${2}"
                        shift 2
                        ;;
                -c | --command)
                        command_to_run="${2}"
                        shift 2
                        ;;
                -n | --netvm)
                        netvm="${2}"
                        shift 2
                        ;;
                -d | --tempdir)
                        if [ -d "${2}" ]; then
                                error "${2}:" 'Directory exists'
                                exit 1
                        fi
                        tempdir="${2}"
                        shift 2
                        ;;
                -s | --tempsize)
                        # TODO: Validate size value
                        # Show error message if size exceeds
                        # available memory
                        tempsize="${2}"
                        shift 2
                        ;;
                -m | --memory)
                        # TODO: Validate memory value
                        # Show error message if memory exceeds
                        # available memory or tempsize
                        memory="${2}"
                        shift 2
                        ;;
                -v | --default_dispvm)
                        # TODO: Show error message if a template
                        # does not exist
                        default_dispvm="${2}"
                        shift 2
                        ;;
				-k | --kernel)
						kernel="${2}"
						shift 2
						;;
				-l | --label)
                        # TODO: Show error message if a label
                        # does not exist
						label="${2}"
						shift 2
						;;
				-e | --ephemeral)
						ephemeral="${2}"
						shift 2
						;;
                --) # End of all options
                        shift
                        break;
                        ;;
                -*)
                        error 'Unknown option:' "${1}"
                        exit 1
                        ;;
                *)  # No more options
                        break
                        ;;
        esac
done
set -u
sudo swapoff --all
sudo mkdir --parents "${tempdir}"

sudo mount --types tmpfs \
           --options size="${tempsize}" \
           "${pool_name}" \
           "${tempdir}"
qvm-pool add "${pool_name}" \
         file \
         --option revisions_to_keep=1 \
         --option dir_path="${tempdir}"

# Create void symlinks to prevent log saving
logdir='/var/log'
logfiles=("${logdir}/libvirt/libxl/${qube_name}.log"
          "${logdir}/qubes/guid.${qube_name}.log"
          "${logdir}/qubes/qrexec.${qube_name}.log"
          "${logdir}/qubes/qubesdb.${qube_name}.log"
          "${logdir}/xen/console/guest-${qube_name}.log")
for file in "${logfiles[@]}"; do
        sudo ln -sfT /dev/null "${file}"
done

qvm-create --class DispVM \
           "${qube_name}" \
           -P "${pool_name}" \
           --template="${template}" \
           --label="${label}" \
           --property netvm="${netvm}" \
           --property memory="${memory}" \
           --property default_dispvm="${default_dispvm}" \
           --property kernel="${kernel}"

case "${ephemeral}" in
	true| TRUE)
		qvm-volume config "${qube_name}":root rw 0
		qvm-volume config "${qube_name}":private rw 0
		qvm-volume config "${qube_name}":volatile ephemeral 1
		;;
	*)
		;;
esac

qvm-run "${qube_name}" "${command_to_run}" && wait

qvm-kill "${qube_name}"
qvm-remove --force "${qube_name}"
qvm-pool remove "${pool_name}"
sudo umount "${pool_name}"

# Leave no trace on file system
sudo rm -rf "${tempdir}"
for file in "${logfiles[@]}"; do
        sudo rm -rf "${file}" "${file}.old"
done

notify-send --expire-time 5000 \
            "${qube_name} qube" \
            "${qube_name} qube remnants cleared."