Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Qemu install script

The script that the QEMU and Whonix guide has you run in steps 1 and 2. It checks whether your computer can run virtual machines at full speed, installs QEMU or updates it, and adds you to the kvm group if you need it. Hover over the code block and press the copy button to take all of it, then save it in your Downloads folder as qemu-install.sh. It is plain text, so read it before you run it; the comments at the top say what each command does. Run it as yourself, not with sudo, and start with bash qemu-install.sh check, which changes nothing.

#!/bin/bash
# qemu-install, v6
#
# Installs QEMU, the program that runs virtual machines, or updates it to
# the newest version your system offers. If you can't use your processor's
# hardware virtualization yet, it also adds you to the kvm group, so
# virtual machines can use it and run at full speed.
#
# There is nothing to edit. Run it as yourself, not with sudo; it asks for
# your password when it needs it.
#
#   qemu-install check     says whether this computer can run fast virtual
#                          machines, and what to fix if it can't; changes
#                          nothing
#   qemu-install install   installs QEMU, or updates it if it is already
#                          there, and adds you to the kvm group if you
#                          need it; apt shows what it will change and asks
#                          first
#
# Run install again whenever you want the newest QEMU. apt downloads only
# what is new or missing and leaves up-to-date packages alone. You only
# need to log out and back in if install ends by saying so.
#
# It installs four packages. qemu-system-x86 runs Intel and AMD virtual
# machines, qemu-system-gui shows them in a window, qemu-utils makes and
# converts disk images, and ovmf lets virtual machines start with UEFI,
# which some operating systems need. apt's optional extras (its
# "recommends") are left out.

set -euo pipefail

## ─── constants ──────────────────────────────────────────────────────

SELF="$(readlink -f "${BASH_SOURCE[0]}")"
readonly SELF

# All four are on the Whonix project's KVM install list, so installing
# Whonix afterwards adds to this set and replaces nothing.
readonly PACKAGES=(qemu-system-x86 qemu-system-gui qemu-utils ovmf)
readonly GROUP='kvm'

## ─── output ─────────────────────────────────────────────────────────

say() { printf '%s\n' "$*"; }
warn() { printf '%s\n' "$*" >&2; }
die() { printf '%s\n' "$1" >&2; exit "${2:-1}"; }

## ─── identity ───────────────────────────────────────────────────────

# Reads line 2 of this file so the filename, header and version output
# cannot drift apart. readlink -f above makes this correct when the script
# is reached through a symlink on PATH.
version() {
    local line
    line="$(awk 'NR==2' "$SELF")" || die "cannot read $SELF"
    printf '%s\n' "${line#\# }"
}

usage() {
    local name
    name="$(basename "$SELF")"
    cat <<EOF
$(version)

installs qemu so you can run virtual machines, or updates it to the
newest version. if you need it, it also adds you to the kvm group so
they run at full speed.

  $name check
      check whether this computer can run fast virtual machines
  $name install
      install or update qemu; apt shows what it will change and asks first
  $name install --yes
      the same, without apt's question
  $name help
      show this help
  $name version
      show which version this is

run it as yourself, not with sudo; it asks for your password when it
needs it. if install ends by telling you to log out and back in, do
that, then run check. run install again any time to get the newest
qemu; it downloads only what has changed.
EOF
}

## ─── preconditions ──────────────────────────────────────────────────

# Refuses root because the kvm group has to go to the person who will run
# the virtual machines, and under sudo id -un answers root instead.
require_environment() {
    local arch
    [[ "$EUID" -ne 0 ]] \
        || die "run this as yourself, not with sudo; it asks for your password when it needs it" 2
    arch="$(uname -m)"
    case "$arch" in
        x86_64) ;;
        *) die "this installs the x86_64 emulator; this host reports $arch" ;;
    esac
    command -v apt-get >/dev/null || die "no apt-get; this script targets Debian and Devuan"
}

# Maps the cpu vendor to the names the firmware setting and the kvm module
# actually use, so the hints below name the right thing on either vendor
# rather than assuming one.
cpu_vendor() {
    local vendor
    vendor="$(awk -F': ' '/^vendor_id/ {print $2; exit}' /proc/cpuinfo)" || vendor=''
    case "$vendor" in
        GenuineIntel) printf '%s\n' 'intel' ;;
        AuthenticAMD) printf '%s\n' 'amd' ;;
        *) printf '%s\n' 'unknown' ;;
    esac
}

# Two different questions. An account is in a group from the moment
# usermod runs; a login session only has the group after a fresh login.
account_in_group() {
    local groups
    groups=" $(id -nG "$1") " || return 1
    [[ "$groups" == *" $2 "* ]]
}

session_in_group() {
    local groups
    groups=" $(id -nG) " || return 1
    [[ "$groups" == *" $1 "* ]]
}

# Returns 0 when the cpu and the kvm module are ready. Each failing branch
# names the next step, because these states are otherwise silent: qemu
# without -enable-kvm runs the guest under emulation and never says why
# it is slow.
check_hardware() {
    local flags vendor setting module
    flags="$(grep -Ec '(vmx|svm)' /proc/cpuinfo)" || flags=0
    vendor="$(cpu_vendor)"

    case "$vendor" in
        intel) setting='intel virtualization technology, or vt-x'; module='kvm_intel' ;;
        amd) setting='svm mode, or amd-v'; module='kvm_amd' ;;
        *) setting='virtualization, vt-x, or svm'; module='kvm_intel or kvm_amd' ;;
    esac

    if [[ "$flags" -eq 0 ]]; then
        warn "no vmx or svm flag in /proc/cpuinfo"
        warn "hardware virtualization is disabled in firmware, or absent from this cpu"
        warn "enter firmware setup at boot; the key varies by vendor, commonly del, f2, f10 or f1"
        warn "the setting is usually under security or advanced, named: $setting"
        return 1
    fi

    if [[ ! -e /dev/kvm ]]; then
        warn "cpu reports the flag but /dev/kvm is missing, so no kvm module is loaded"
        warn "try: sudo modprobe $module"
        warn "then read: sudo dmesg | grep -i kvm"
        warn "if it says disabled by bios, turn this on in firmware setup: $setting"
        return 1
    fi

    return 0
}

# Returns 0 when this login can use /dev/kvm. The failures need different
# fixes, and telling someone to join a group they are already in sends
# them round in a circle.
check_access() {
    local user
    if [[ -r /dev/kvm && -w /dev/kvm ]]; then
        say "hardware acceleration is available"
        return 0
    fi

    user="$(id -un)"
    if session_in_group "$GROUP"; then
        warn "you are in the $GROUP group, but /dev/kvm still does not let you read and write it"
        warn "look at its permissions with: ls -l /dev/kvm"
    elif account_in_group "$user" "$GROUP"; then
        warn "you are in the $GROUP group, but it takes effect only after you log out and back in"
    else
        warn "/dev/kvm exists but you cannot read and write it"
        warn "run '$(basename "$SELF") install' to join the $GROUP group, then start a new login session"
    fi
    return 1
}

## ─── install ────────────────────────────────────────────────────────

# apt-get installs what is missing, upgrades what is out of date and leaves
# the rest alone, reusing packages it has already downloaded. Handing it the
# whole list every time is what makes a later run an update.
install_packages() {
    local assume_yes="$1"
    local rc version
    local -a answer=()

    # apt asks before it changes anything and gives up when nobody can
    # answer; this says so up front instead of after the package-list refresh.
    if [[ "$assume_yes" == 'yes' ]]; then
        answer=(-y)
    elif [[ ! -t 0 ]]; then
        die "no terminal to prompt and --yes was not given; nothing installed or updated" 2
    fi

    command -v sudo >/dev/null || die "no sudo on PATH; cannot install packages"
    sudo -v || die "sudo failed, so nothing was installed"

    say "refreshing package lists..."
    # --error-on=any stops on any list that failed to refresh, so a run that
    # gets past here really has checked for the newest versions.
    if ! sudo apt-get update --error-on=any; then
        warn "apt-get update failed; the lines above name the package source that failed"
        die "check your internet connection, or fix or remove that source, then run this again"
    fi

    say "installing or updating qemu..."
    rc=0
    sudo apt-get install --no-install-recommends "${answer[@]}" "${PACKAGES[@]}" || rc=$?

    # apt-get exits 1 when its question is answered no, and 100 on errors.
    case "$rc" in
        0) ;;
        1) die "apt stopped without changing anything; nothing was installed or updated" ;;
        *)
            warn "apt could not finish; see its messages above"
            die "if apt could not find a package, this copy of qemu-install may be too old for your system; get a newer copy"
            ;;
    esac

    version="$(qemu-system-x86_64 --version)" \
        || die "qemu was installed but does not start; see the error above"
    say "qemu is up to date: ${version%%$'\n'*}"
}

# Changes nothing when this login can already use /dev/kvm, or already has
# the group, because then logging out would fix nothing. Otherwise the
# log-out line is printed whenever the account has the group but this
# login does not yet, including on a second run before logging out.
join_group() {
    local user
    user="$(id -un)"

    if [[ -r /dev/kvm && -w /dev/kvm ]] || session_in_group "$GROUP"; then
        return 0
    fi
    if ! getent group "$GROUP" >/dev/null; then
        warn "group $GROUP does not exist; skipping"
        return 0
    fi
    if ! account_in_group "$user" "$GROUP"; then
        say "adding $user to $GROUP..."
        sudo usermod -aG "$GROUP" "$user" || die "could not add $user to $GROUP"
    fi
    say "log out and back in before $GROUP takes effect"
}

## ─── dispatch ───────────────────────────────────────────────────────

main() {
    local subcommand="${1:-}"
    local assume_yes='no'

    case "$subcommand" in
        ''|help|-h|--help) usage; return 0 ;;
        version|--version) version; return 0 ;;
        check|install) shift ;;
        *) die "unknown argument: $subcommand (try: $(basename "$SELF") help)" 2 ;;
    esac

    while [[ "$#" -gt 0 ]]; do
        case "$1" in
            -y|--yes) assume_yes='yes' ;;
            *) die "unknown flag: $1 (try: $(basename "$SELF") help)" 2 ;;
        esac
        shift
    done

    require_environment

    case "$subcommand" in
        check)
            check_hardware || return 1
            check_access || return 1
            ;;
        install)
            if ! check_hardware; then
                warn "installing anyway; virtual machines will be slow or fail to start until this is fixed"
            fi
            install_packages "$assume_yes"
            join_group
            ;;
    esac
}

main "$@"