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 unnecessarywait
. 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