Automate safe system upodates with a single script (for APT + systemd systems)

THE PROBLEM Keeping a Linux system fully updated usually means doing several things by hand: Update APT package lists Upgrade installed packages Remove unused dependencies and cached files Update Flatpak apps (if you use Flatpak) Update firmware via fwupd (if available) Decide whether to reboot or shut down None of that is hard, but it is repetitive and easy to skip steps, especially firmware updates. This script turns that whole workflow into a single, safe command. REQUIREMENTS This script assumes: Package manager Uses APT Example: Debian, Ubuntu, Linux Mint and similar Init system Uses systemd (for systemctl reboot/poweroff) Shell bash (script uses “#!/usr/bin/env bash” and “set -euo pipefail”) You can run it with: bash script.sh Privileges Your user has sudo rights Optional components Flatpak (optional) If not installed, Flatpak steps are skipped fwupd (fwupdmgr, optional) If not installed, firmware steps a...

Why choosing QEMU directly instead of the virtualisation stack

Why I chose QEMU directly instead of the virtualisation stack

Today I finally settled the question: QEMU directly vs libvirt/virt-manager, especially for a Windows work VM stored on a USB stick.

Short answer: for this use case, QEMU + one good script beats the whole virtualisation stack.

My context

  • The VM disk (LCS.raw) lives on a USB partition (label: LCS_RAW).

  • I want a “VM on a stick”: plug USB anywhere → mount → run → done.

  • It’s one VM, always the same, for client work (Windows + browser).

  • I don’t need snapshots, multi-VM orchestration, XML configs, etc.

Why I didn’t want libvirt / virt-manager here

1. libvirt assumes “local, permanent storage”

Libvirt stores VM definitions in XML pointing to fixed paths like:

  • /var/lib/libvirt/images/windows.qcow2

My VM is on a removable USB, which might be:

  • /dev/sda2 today

  • /dev/sdb2 tomorrow

  • /media/ernest/Whatever if Plasma mounts it

  • /mnt/LCS when I mount it manually

Libvirt and virt-manager don’t like that kind of instability. They want:

  • stable devices

  • stable paths

  • stable XML definitions

A “VM on a stick” is fundamentally not that.


2. One VM doesn’t need a whole orchestration layer

libvirt + virt-manager are great when:

  • I have many VMs.

  • I want autostart on boot.

  • I need fancy network topologies.

  • I want to manage VMs like services.

Here, I have exactly one VM.
I just want a tool that launches it reliably.


3. Fewer layers = fewer things to break

With libvirt:

virt-manager → libvirt → QEMU → KVM → VM

With my approach:

lcs → QEMU → KVM → VM

No background daemons, no XML, no GUI tool that can silently “forget” the VM because the path changed. Just one script that does exactly what I tell it to.


4. Portability

If I move to another machine:

  • I install QEMU/KVM.

  • I copy ~/.local/bin/lcs.

  • I plug the same USB.

  • I run lcs.

No need to re-define libvirt domains, re-import images, fix XML paths, etc.


5. Cleaner mental model

  • Scripts (update.sh, autorotate-wayland.sh) = automation I edit and run.

  • Commands (lcs) = tools that feel native, in ~/.local/bin, no extension.

lcs is a command that knows how to find, mount and launch my VM.
I don’t need a whole “virtualisation stack” to get that.


The design of lcs (my QEMU launcher)

lcs is a Bash script in:

~/.local/bin/lcs

It is a script disguised as a command.
When I type lcs, it:

  1. Finds the VM partition by label (LCS_RAW), not by /dev/sdX2.

  2. Reuses any existing mount (e.g. /media/ernest/...) or mounts it at /mnt/LCS.

  3. Builds sane defaults for CPU/RAM based on the host.

  4. Detects KVM and uses hardware acceleration if possible.

  5. Starts QEMU with:

    • IDE disk (safe default)

    • e1000 network card (works in Windows without drivers)

    • GTK display

    • optional SPICE, virtio disk/net, port forwarding, and virtio ISO

  6. Logs everything to ~/.local/share/lcs/DATE_lcs.log.

  7. Cleans up by auto-unmounting only if it mounted the partition.

It also has a CLI and some environment overrides so I can tune things without changing the script.


The lcs script (current version)

Note to future me: this looks long, but it’s basically:

mount → sanity checks → build QEMU args → run → cleanup.


*****************************************


#!/usr/bin/env bash

# LCS Windows VM Launcher (with autodetect + CLI)

# - Reuses any existing mount of LCS_RAW (no duplicate mounts)

# - Only unmounts on exit if this script mounted it

# - Long-option CLI: --help, --spice, --smp, --ram, --virtio-disk, --virtio-net, --e1000, --fwd, --virtio-iso, --device, --vmfile, --no-autounmount

# - Still supports env overrides for power users


set -Eeuo pipefail


usage() {

  cat <<'EOF'

lcs - Launch Windows VM from the LCS_RAW USB partition


Usage: lcs [options]


Display & UI:

  --help                 Show this help and exit

  --gtk                  Use GTK display (default)

  --spice [PORT]         Enable SPICE (default port 5930 if PORT omitted)


CPU / RAM:

  --smp N                Set vCPUs (default: min(4, nproc))

  --ram SIZE             Set RAM (e.g. 8G, 8192M; default ~60% host, min 4G, max 16G)


Disk / Network:

  --virtio-disk          Use virtio block device (needs Windows virtio storage driver)

  --virtio-net           Use virtio NIC (needs Windows virtio net driver)

  --e1000                Use Intel e1000 NIC (default; no extra drivers)

  --fwd SPEC             Host forwards, e.g. "tcp::13389-:3389,tcp::1222-:22"


Media:

  --virtio-iso PATH      Attach virtio-win drivers ISO (forces attachment)


Device / File:

  --device PATH          Override block device (default /dev/disk/by-label/LCS_RAW)

  --vmfile PATH          Override VM image path (default <mountpoint>/LCS.raw)


Behaviour:

  --no-autounmount       Do NOT unmount on exit (even if we mounted it)


Notes:

- The launcher auto-detects if the LCS_RAW partition is already mounted anywhere

  (e.g. /media/$USER/...), and reuses that mountpoint. If it's already mounted,

  we will NOT unmount it on exit. If we mount it, we will auto-unmount on exit.

EOF

}


### ---- Defaults (env can override) -----------------------------------------

MOUNTPOINT="${MOUNTPOINT:-/mnt/LCS}"

DEVICE="${DEVICE:-/dev/disk/by-label/LCS_RAW}"   # stable by label

LCS_SPICE="${LCS_SPICE:-0}"

SPICE_PORT="${SPICE_PORT:-5930}"

LCS_DISK_IF="${LCS_DISK_IF:-ide}"                # 'ide' (safe) or 'virtio'

LCS_VIRTIO_NET="${LCS_VIRTIO_NET:-0}"            # 1 => virtio-net, else e1000

LCS_FWD="${LCS_FWD:-}"

LCS_ATTACH_VIRTIO="${LCS_ATTACH_VIRTIO:-0}"

VIRTIO_ISO="${VIRTIO_ISO:-}"

NO_AUTOUNMOUNT="${NO_AUTOUNMOUNT:-0}"


# Logging

LOGDIR="${LOGDIR:-$HOME/.local/share/lcs}"

mkdir -p "$LOGDIR"

LOGFILE="$LOGDIR/$(date +%F_%H-%M-%S)_lcs.log"

exec > >(tee -a "$LOGFILE") 2>&1

echo "[LCS] Log => $LOGFILE"


### ---- CLI parsing ---------------------------------------------------------

while [[ $# -gt 0 ]]; do

  case "$1" in

    -h|--help) usage; exit 0 ;;

    --gtk) LCS_SPICE=0 ;;

    --spice) LCS_SPICE=1; if [[ ${2:-} =~ ^[0-9]+$ ]]; then SPICE_PORT="$2"; shift; fi ;;

    --spice-port) SPICE_PORT="${2:-$SPICE_PORT}"; shift ;;

    --spice-port=*) SPICE_PORT="${1#*=}" ;;

    --smp) LCS_SMP="${2}"; shift ;;

    --smp=*) LCS_SMP="${1#*=}" ;;

    --ram) LCS_RAM="${2}"; shift ;;

    --ram=*) LCS_RAM="${1#*=}" ;;

    --virtio-disk) LCS_DISK_IF="virtio" ;;

    --virtio-net) LCS_VIRTIO_NET=1 ;;

    --e1000) LCS_VIRTIO_NET=0 ;;

    --fwd) LCS_FWD="${2}"; shift ;;

    --fwd=*) LCS_FWD="${1#*=}" ;;

    --virtio-iso) VIRTIO_ISO="${2}"; LCS_ATTACH_VIRTIO=1; shift ;;

    --virtio-iso=*) VIRTIO_ISO="${1#*=}"; LCS_ATTACH_VIRTIO=1 ;;

    --device) DEVICE="${2}"; shift ;;

    --device=*) DEVICE="${1#*=}" ;;

    --vmfile) VMFILE_OVERRIDE="${2}"; shift ;;

    --vmfile=*) VMFILE_OVERRIDE="${1#*=}" ;;

    --no-autounmount) NO_AUTOUNMOUNT=1 ;;

    *) echo "[LCS] Unknown option: $1"; usage; exit 2 ;;

  esac

  shift

done


### ---- Sanity checks -------------------------------------------------------

command -v qemu-system-x86_64 >/dev/null || { echo "[LCS] ERROR: qemu-system-x86_64 not found"; exit 1; }


# Ensure mount directory exists for our preferred mountpoint

[[ -d "$MOUNTPOINT" ]] || sudo mkdir -p "$MOUNTPOINT"


### ---- Auto-detect if device is already mounted anywhere -------------------

ACTIVE_MP=""

if command -v findmnt >/dev/null; then

  ACTIVE_MP="$(findmnt -rn -S "$DEVICE" -o TARGET || true)"

  REALDEV="$(readlink -f "$DEVICE" || true)"

  if [[ -z "$ACTIVE_MP" && -n "$REALDEV" ]]; then

    ACTIVE_MP="$(findmnt -rn -S "$REALDEV" -o TARGET || true)"

  fi

else

  REALDEV="$(readlink -f "$DEVICE" || true)"

  ACTIVE_MP="$(awk -v dev="${REALDEV:-$DEVICE}" '$1==dev {print $2}' /proc/self/mounts | head -n1)"

fi


mounted_here=0

if [[ -n "$ACTIVE_MP" ]]; then

  echo "[LCS] Reusing existing mount: $ACTIVE_MP"

  MOUNTPOINT="$ACTIVE_MP"

else

  echo "[LCS] Mounting $DEVICE at $MOUNTPOINT ..."

  sudo mount "$DEVICE" "$MOUNTPOINT"

  mounted_here=1

fi


# Decide VM file path (allow override)

VMFILE="${VMFILE_OVERRIDE:-$MOUNTPOINT/LCS.raw}"


cleanup() {

  st=$?

  if (( mounted_here == 1 )) && (( NO_AUTOUNMOUNT == 0 )); then

    echo "[LCS] Sync & unmount $MOUNTPOINT ..."

    sync || true

    sudo umount "$MOUNTPOINT" || true

  else

    [[ $mounted_here -eq 1 && $NO_AUTOUNMOUNT -eq 1 ]] && echo "[LCS] Skipping auto-unmount (--no-autounmount)."

  fi

  exit $st

}

trap cleanup EXIT INT TERM


### ---- Validate VM image ---------------------------------------------------

if [[ ! -f "$VMFILE" ]]; then

  echo "[LCS] ERROR: VM file not found: $VMFILE"

  exit 1

fi


### ---- Auto-detect CPU/RAM defaults (overridable) --------------------------

TOTAL_CPUS=$(nproc)

SMP="${LCS_SMP:-$(( TOTAL_CPUS>4 ? 4 : TOTAL_CPUS ))}"


TOTAL_MEM_KB=$(grep -E '^MemTotal:' /proc/meminfo | awk '{print $2}')

HOST_MB=$(( TOTAL_MEM_KB / 1024 ))

DEFAULT_VM_MB=$(( HOST_MB * 60 / 100 ))   # 60% host

(( DEFAULT_VM_MB < 4096 )) && DEFAULT_VM_MB=4096

(( DEFAULT_VM_MB > 16384 )) && DEFAULT_VM_MB=16384

RAM="${LCS_RAM:-${DEFAULT_VM_MB}M}"


### ---- KVM detection -------------------------------------------------------

KVM_ARGS=()

if [[ -e /dev/kvm ]] && [[ -r /dev/kvm ]] && id -nG "$USER" | grep -qw kvm; then

  echo "[LCS] KVM available: hardware acceleration ON."

  KVM_ARGS+=( -enable-kvm -cpu host )

else

  echo "[LCS] KVM not available (or user not in 'kvm'); using tcg."

  KVM_ARGS+=( -cpu qemu64 )

fi


### ---- Build QEMU args -----------------------------------------------------

QEMU_ARGS=( "${KVM_ARGS[@]}" -smp "$SMP" -m "$RAM" )


# Display

if [[ "$LCS_SPICE" == "1" ]]; then

  echo "[LCS] SPICE display enabled on port $SPICE_PORT"

  QEMU_ARGS+=( -display gtk )

  QEMU_ARGS+=( -spice "port=${SPICE_PORT},disable-ticketing=on" )

  QEMU_ARGS+=( -device virtio-vga )

  QEMU_ARGS+=(

    -device virtio-serial-pci

    -chardev spicevmc,id=vdagent0,name=vdagent

    -device virtserialport,chardev=vdagent0,name=com.redhat.spice.0

  )


else

  QEMU_ARGS+=( -display gtk )

fi


# Disk

if [[ "$LCS_DISK_IF" == "virtio" ]]; then

  echo "[LCS] Disk bus: virtio"

  QEMU_ARGS+=( -drive id=drv0,file="$VMFILE",format=raw,if=none

               -device virtio-blk-pci,drive=drv0 )

else

  echo "[LCS] Disk bus: IDE (safe)"

  QEMU_ARGS+=( -drive id=drv0,file="$VMFILE",format=raw,if=none

               -device ide-hd,drive=drv0,bus=ide.0 )

fi


# Network (+ optional forwards)

NETDEV_OPTS="user,id=n0"

if [[ -n "$LCS_FWD" ]]; then

  echo "[LCS] Port forwards: $LCS_FWD"

  NETDEV_OPTS="$NETDEV_OPTS,hostfwd=$LCS_FWD"

fi

QEMU_ARGS+=( -netdev "$NETDEV_OPTS" )

if [[ "$LCS_VIRTIO_NET" == "1" ]]; then

  echo "[LCS] NIC: virtio-net"

  QEMU_ARGS+=( -device virtio-net,netdev=n0 )

else

  echo "[LCS] NIC: e1000 (compat)"

  QEMU_ARGS+=( -device e1000,netdev=n0 )

fi


# Virtio ISO (attach if requested or auto-found)

attach_virtio=0

if [[ "$LCS_ATTACH_VIRTIO" == "1" ]]; then

  attach_virtio=1

else

  for iso in "$VIRTIO_ISO" \

             "$HOME/Downloads/virtio-win.iso" \

             "/usr/share/virtio-win/virtio-win.iso" \

             "/usr/share/virtio-win/virtio-win-guest-tools.iso"; do

    [[ -n "$iso" && -f "$iso" ]] && { VIRTIO_ISO="$iso"; attach_virtio=1; break; }

  done

fi

if (( attach_virtio == 1 )); then

  if [[ -n "$VIRTIO_ISO" ]]; then

    echo "[LCS] Attaching virtio ISO: $VIRTIO_ISO"

    QEMU_ARGS+=( -drive "file=$VIRTIO_ISO,media=cdrom,readonly=on" )

  else

    echo "[LCS] Note: --virtio-iso PATH to attach specific ISO."

  fi

fi


### ---- Run ---------------------------------------------------------------

echo "[LCS] Using mountpoint: $MOUNTPOINT"

echo "[LCS] VM image: $VMFILE"

echo "[LCS] QEMU:"

printf '  %q ' qemu-system-x86_64 "${QEMU_ARGS[@]}"; echo


qemu-system-x86_64 "${QEMU_ARGS[@]}"

echo "[LCS] VM exited with code $?"


*****************************************



How I actually use it (cheat sheet)

For future me:

  • Normal run:

    lcs
  • With SPICE:

    lcs --spice
  • Force RAM/CPU:

    lcs --ram 8G --smp 4
  • Forward RDP:

    lcs --fwd "tcp::13389-:3389"
  • Attach virtio ISO (while installing drivers in Windows):

    lcs --virtio-iso ~/Downloads/virtio-win.iso
  • Toggle fullscreen inside the VM window:

    Ctrl + Alt + f

This setup gives me a portable Windows work VM on a USB stick, powered by QEMU directly, without having to drag libvirt/virt-manager into the story.

 

Comments