Ik draai constant VMs. Kubernetes deployments testen, een distro uitproberen waar ik over las, Windows draaien voor die ene koppige applicatie, een experiment isoleren zodat ik mijn echte machine niet sloop. Voor dat alles is KVM/QEMU de juiste tool. Near-native performance, volledig open source, ingebakken in de Linux kernel. Niets dat naar huis belt, niets om te licentiëren, geen vendor die bepaalt wat ik met mijn eigen hardware mag doen.

De commando’s, dat is waar het misgaat.

virt-install --name test --ram 2048 --vcpus 2 --disk path=/var/lib/libvirt/images/test.qcow2,size=20 --cdrom /home/user/Downloads/ubuntu-22.04.iso --os-variant ubuntu22.04 --network bridge=virbr0 --graphics spice --console pty,target_type=serial

Niemand typt dat uit zijn hoofd. Je kopieert het van een wiki, verwisselt een paar waardes, krijgt het disk-pad fout, en probeert opnieuw. Tegen de tijd dat de VM boot, is de nieuwsgierigheid die je iets wilde laten testen alweer afgekoeld. Dat gat, tussen “ik zou dit even in een VM moeten checken” en de VM die echt draait, is precies waar goede voornemens sterven.

Dus op een gegeven moment hield ik op met vechten en schreef ik een kleine stapel scripts en aliases. Snelle VM creatie vanaf een ISO met defaults waar ik nooit over hoef na te denken, snapshots die één woord kosten, beheer dat geen handleiding vereist. De lat die ik legde was simpel: als ik het sneller kan typen dan ik door virt-manager kan klikken, gebruik ik het echt. Alles hieronder haalt die lat.

Waarom scripts en niet de GUI

Ik leun op dit soort tooling om redenen die ik uitwerkte in Working with an AuDHD Brain. Elke GUI verstopt zijn functionaliteit achter een layout die ik elke keer opnieuw in mijn hoofd moet laden. Waar zat de snapshot-knop ook alweer? Welk tabblad had de disk-instellingen? Die zoektocht kost me focus die ik liever aan het echte probleem besteed.

Een script laat me niets onthouden. Het draait elke keer hetzelfde, het bestand zelf documenteert wat het doet, tab completion is sneller dan welke muis ook, en ik kan de ene in de andere pipen of vanuit een groter script aanroepen zonder erbij na te denken.

De saaie praktische winst telt ook: scripts werken over SSH. De meeste van mijn VMs leven op een headless bak in de hoek, en daar heb je weinig aan virt-manager.

Begin met de aliases

De aliases zijn de goedkoopst mogelijke winst, dus begin daar. Gooi deze in je .bashrc of .zshrc:

# Snelle status
alias vms='virsh list --all'
alias vmsr='virsh list'  # Alleen draaiend
alias vmnets='virsh net-list --all'
alias vmpools='virsh pool-list --all'

# Power management
alias vmstart='virsh start'
alias vmstop='virsh shutdown'
alias vmkill='virsh destroy'  # Forceer stop
alias vmreboot='virsh reboot'

# Console toegang
alias vmconsole='virsh console'
alias vmviewer='virt-viewer'

# Snelle info
alias vminfo='virsh dominfo'
alias vmip='virsh domifaddr'
alias vmblk='virsh domblklist'

# Snapshots
alias vmsnap='virsh snapshot-create-as'
alias vmsnaps='virsh snapshot-list'
alias vmrevert='virsh snapshot-revert'
alias vmsnapdel='virsh snapshot-delete'

# Edit
alias vmedit='virsh edit'

Nu wordt virsh list --all gewoon vms. Op zichzelf stelt dat niets voor. Maar je draait een handvol van deze tientallen keren per dag, en de bespaarde toetsaanslagen plus de bespaarde “wat was die flag ook alweer” zoektochten veranderen stilletjes hoe vaak je überhaupt naar VMs grijpt.

Het werkpaard: vmquick

Aliases verkorten dingen die je al kent. De echte frictie zit in het aanmaken, dus dit is het script dat ik het meest gebruik. Maak een VM van een ISO met zo min mogelijk typen:

#!/bin/bash
# vmquick - Snelle VM creatie met verstandige defaults
# Gebruik: vmquick <naam> <iso-path> [ram-gb] [cpus] [disk-gb]

set -euo pipefail

# Defaults (makkelijk later te upgraden)
DEFAULT_RAM=2       # GB
DEFAULT_CPUS=2
DEFAULT_DISK=20     # GB
VM_PATH="/var/lib/libvirt/images"
NETWORK="default"   # Of je bridge naam

# Kleuren voor output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # Geen kleur

usage() {
    echo "Gebruik: vmquick <naam> <iso-path> [ram-gb] [cpus] [disk-gb]"
    echo ""
    echo "Voorbeelden:"
    echo "  vmquick ubuntu ubuntu-22.04.iso              # 2GB RAM, 2 CPUs, 20GB disk"
    echo "  vmquick fedora fedora-39.iso 4               # 4GB RAM"
    echo "  vmquick arch archlinux.iso 4 4 50            # 4GB RAM, 4 CPUs, 50GB disk"
    echo ""
    echo "Defaults: ${DEFAULT_RAM}GB RAM, ${DEFAULT_CPUS} CPUs, ${DEFAULT_DISK}GB disk"
    exit 1
}

# Check argumenten
[[ $# -lt 2 ]] && usage

NAME="$1"
ISO="$2"
RAM="${3:-$DEFAULT_RAM}"
CPUS="${4:-$DEFAULT_CPUS}"
DISK="${5:-$DEFAULT_DISK}"

# Converteer RAM naar MB voor virt-install
RAM_MB=$((RAM * 1024))

# Valideer dat ISO bestaat
if [[ ! -f "$ISO" ]]; then
    # Check veelgebruikte download locaties
    for dir in ~/Downloads ~/ISOs /tmp; do
        if [[ -f "$dir/$ISO" ]]; then
            ISO="$dir/$ISO"
            break
        fi
    done
fi

if [[ ! -f "$ISO" ]]; then
    echo -e "${RED}Fout: ISO niet gevonden: $ISO${NC}"
    echo "Gecheckt: $ISO, ~/Downloads/$ISO, ~/ISOs/$ISO, /tmp/$ISO"
    exit 1
fi

# Check of VM al bestaat
if virsh dominfo "$NAME" &>/dev/null; then
    echo -e "${RED}Fout: VM '$NAME' bestaat al${NC}"
    echo "Gebruik 'vmrm $NAME' om deze eerst te verwijderen"
    exit 1
fi

DISK_PATH="${VM_PATH}/${NAME}.qcow2"

echo -e "${GREEN}VM aanmaken: $NAME${NC}"
echo "  RAM:  ${RAM}GB (${RAM_MB}MB)"
echo "  CPUs: $CPUS"
echo "  Disk: ${DISK}GB op $DISK_PATH"
echo "  ISO:  $ISO"
echo ""

# Detecteer OS variant (best effort)
OS_VARIANT="linux2022"
case "$ISO" in
    *ubuntu*22*|*jammy*)     OS_VARIANT="ubuntu22.04" ;;
    *ubuntu*24*|*noble*)     OS_VARIANT="ubuntu24.04" ;;
    *ubuntu*20*|*focal*)     OS_VARIANT="ubuntu20.04" ;;
    *debian*12*|*bookworm*)  OS_VARIANT="debian12" ;;
    *debian*11*|*bullseye*)  OS_VARIANT="debian11" ;;
    *fedora*39*)             OS_VARIANT="fedora39" ;;
    *fedora*40*)             OS_VARIANT="fedora40" ;;
    *arch*)                  OS_VARIANT="archlinux" ;;
    *alpine*)                OS_VARIANT="alpinelinux3.18" ;;
    *centos*9*|*stream*9*)   OS_VARIANT="centos-stream9" ;;
    *rocky*9*)               OS_VARIANT="rocky9" ;;
    *alma*9*)                OS_VARIANT="almalinux9" ;;
    *windows*11*)            OS_VARIANT="win11" ;;
    *windows*10*)            OS_VARIANT="win10" ;;
    *talos*)                 OS_VARIANT="linux2022" ;;
esac

echo -e "${YELLOW}Gedetecteerde OS variant: $OS_VARIANT${NC}"
echo ""

# Maak de VM
virt-install \
    --name "$NAME" \
    --ram "$RAM_MB" \
    --vcpus "$CPUS" \
    --disk "path=$DISK_PATH,size=$DISK,format=qcow2,bus=virtio" \
    --cdrom "$ISO" \
    --os-variant "$OS_VARIANT" \
    --network "network=$NETWORK,model=virtio" \
    --graphics spice,listen=none \
    --video virtio \
    --channel spicevmc \
    --console pty,target_type=serial \
    --boot cdrom,hd \
    --noautoconsole

echo ""
echo -e "${GREEN}VM '$NAME' aangemaakt en gestart!${NC}"
echo ""
echo "Volgende stappen:"
echo "  vmview $NAME             # GUI console (virt-viewer)"
echo "  vmcon $NAME              # Seriële console (als OS het ondersteunt)"
echo "  vmip $NAME               # Krijg IP adres (na OS installatie)"
echo ""
echo "Om later te upgraden:"
echo "  vmupgrade $NAME 8        # Zet RAM naar 8GB"
echo "  vmupgrade $NAME 8 4      # Zet RAM naar 8GB, CPUs naar 4"
echo "  vmupgrade $NAME 8 4 50   # Vergroot ook disk naar 50GB"

Sla het op als ~/.local/bin/vmquick, chmod +x het, en je bent klaar.

Gebruiksvoorbeelden

# Minimaal - alleen naam en ISO
vmquick testvm ubuntu-22.04.4-live-server-amd64.iso

# Met meer RAM
vmquick k8s-node talos-amd64.iso 8

# Volledige specs voor een zware workload
vmquick gitlab fedora-server-40.iso 16 8 100

Het script speurt naar de ISO op de gebruikelijke plekken, dus als het bestand in ~/Downloads of ~/ISOs staat geef je gewoon de bestandsnaam en sla je het volledige pad over.

Later opschalen: vmupgrade

Ik start elke VM bewust klein. 2GB en twee cores is ruim genoeg om uit te vinden of iets werkt, en als ik later meer nodig heb is het ophogen één commando in plaats van een herinstallatie. Dat regelt dit script:

#!/bin/bash
# vmupgrade - Upgrade VM resources (vereist dat VM gestopt is)
# Gebruik: vmupgrade <naam> [ram-gb] [cpus] [disk-gb]

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

usage() {
    echo "Gebruik: vmupgrade <naam> [ram-gb] [cpus] [disk-gb]"
    echo ""
    echo "Voorbeelden:"
    echo "  vmupgrade myvm 8           # Zet RAM naar 8GB"
    echo "  vmupgrade myvm 8 4         # Zet RAM naar 8GB, CPUs naar 4"
    echo "  vmupgrade myvm 8 4 50      # Vergroot ook disk naar 50GB"
    echo ""
    echo "Let op: VM moet gestopt zijn. Disk kan alleen vergroot, niet verkleind worden."
    exit 1
}

[[ $# -lt 2 ]] && usage

NAME="$1"
RAM="${2:-}"
CPUS="${3:-}"
DISK="${4:-}"

# Check VM bestaat
if ! virsh dominfo "$NAME" &>/dev/null; then
    echo -e "${RED}Fout: VM '$NAME' niet gevonden${NC}"
    exit 1
fi

# Check VM is gestopt
STATE=$(virsh domstate "$NAME" 2>/dev/null)
if [[ "$STATE" != "shut off" ]]; then
    echo -e "${RED}Fout: VM moet eerst gestopt worden${NC}"
    echo "Huidige staat: $STATE"
    echo "Voer uit: vmstop $NAME"
    exit 1
fi

echo -e "${GREEN}VM upgraden: $NAME${NC}"
echo ""

# Update RAM
if [[ -n "$RAM" ]]; then
    RAM_MB=$((RAM * 1024))
    echo "RAM instellen op ${RAM}GB..."
    virsh setmaxmem "$NAME" "${RAM_MB}M" --config
    virsh setmem "$NAME" "${RAM_MB}M" --config
    echo -e "${GREEN}  RAM: ${RAM}GB${NC}"
fi

# Update CPUs
if [[ -n "$CPUS" ]]; then
    echo "CPUs instellen op $CPUS..."
    virsh setvcpus "$NAME" "$CPUS" --config --maximum
    virsh setvcpus "$NAME" "$CPUS" --config
    echo -e "${GREEN}  CPUs: $CPUS${NC}"
fi

# Vergroot disk
if [[ -n "$DISK" ]]; then
    # Vind de disk
    DISK_PATH=$(virsh domblklist "$NAME" | grep -E '\.qcow2|\.img' | awk '{print $2}' | head -1)

    if [[ -z "$DISK_PATH" ]]; then
        echo -e "${YELLOW}Waarschuwing: Kon geen disk vinden om te resizen${NC}"
    else
        CURRENT_SIZE=$(qemu-img info "$DISK_PATH" | grep 'virtual size' | grep -oP '\d+(?= GiB)')

        if [[ "$DISK" -le "$CURRENT_SIZE" ]]; then
            echo -e "${YELLOW}Waarschuwing: Nieuwe grootte ($DISK GB) is niet groter dan huidige ($CURRENT_SIZE GB)${NC}"
            echo "Disk resize overgeslagen (kan alleen vergroten)"
        else
            echo "Disk vergroten van ${CURRENT_SIZE}GB naar ${DISK}GB..."
            qemu-img resize "$DISK_PATH" "${DISK}G"
            echo -e "${GREEN}  Disk: ${DISK}GB${NC}"
            echo -e "${YELLOW}  Let op: Je moet de partitie in de VM nog uitbreiden${NC}"
        fi
    fi
fi

echo ""
echo -e "${GREEN}Upgrade compleet!${NC}"
echo "Start de VM met: vmstart $NAME"

Snapshots zonder ceremonie: vmsnap

Een snapshot is het vangnet dat me toestaat om met opzet roekeloze dingen te doen. Sloop de VM, zet terug, probeer opnieuw, geen schade. De rauwe virsh snapshot-* commando’s zijn prima maar omslachtig, dus deze wrapper haalt de frictie eruit:

#!/bin/bash
# vmsnap - Makkelijk snapshot beheer
# Gebruik: vmsnap <commando> <vm-naam> [snapshot-naam]

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

usage() {
    echo "Gebruik: vmsnap <commando> <vm-naam> [snapshot-naam]"
    echo ""
    echo "Commando's:"
    echo "  create <vm> [naam]    Maak snapshot (default naam: timestamp)"
    echo "  list <vm>             Toon alle snapshots"
    echo "  revert <vm> [naam]    Terugzetten naar snapshot (default: laatste)"
    echo "  delete <vm> <naam>    Verwijder een snapshot"
    echo "  info <vm> <naam>      Toon snapshot details"
    echo ""
    echo "Voorbeelden:"
    echo "  vmsnap create myvm                    # Maak met timestamp naam"
    echo "  vmsnap create myvm before-upgrade    # Maak met custom naam"
    echo "  vmsnap list myvm                      # Toon alle snapshots"
    echo "  vmsnap revert myvm                    # Terug naar laatste"
    echo "  vmsnap revert myvm before-upgrade    # Terug naar specifieke snapshot"
    exit 1
}

[[ $# -lt 2 ]] && usage

CMD="$1"
VM="$2"
SNAP_NAME="${3:-}"

# Verifieer VM bestaat
if ! virsh dominfo "$VM" &>/dev/null; then
    echo -e "${RED}Fout: VM '$VM' niet gevonden${NC}"
    exit 1
fi

case "$CMD" in
    create)
        # Genereer naam als niet opgegeven
        if [[ -z "$SNAP_NAME" ]]; then
            SNAP_NAME="snap-$(date +%Y%m%d-%H%M%S)"
        fi

        echo -e "${GREEN}Snapshot '$SNAP_NAME' aanmaken voor VM '$VM'...${NC}"
        virsh snapshot-create-as "$VM" "$SNAP_NAME" --description "Aangemaakt door vmsnap op $(date)"
        echo -e "${GREEN}Snapshot aangemaakt!${NC}"
        ;;

    list)
        echo -e "${CYAN}Snapshots voor VM '$VM':${NC}"
        echo ""
        virsh snapshot-list "$VM" --tree 2>/dev/null || virsh snapshot-list "$VM"
        ;;

    revert)
        if [[ -z "$SNAP_NAME" ]]; then
            # Pak de laatste snapshot
            SNAP_NAME=$(virsh snapshot-list "$VM" --name | tail -1)
            if [[ -z "$SNAP_NAME" ]]; then
                echo -e "${RED}Fout: Geen snapshots gevonden voor VM '$VM'${NC}"
                exit 1
            fi
            echo -e "${YELLOW}Geen snapshot opgegeven, gebruik laatste: $SNAP_NAME${NC}"
        fi

        echo -e "${GREEN}VM '$VM' terugzetten naar snapshot '$SNAP_NAME'...${NC}"
        virsh snapshot-revert "$VM" "$SNAP_NAME"
        echo -e "${GREEN}Teruggezet!${NC}"
        ;;

    delete)
        if [[ -z "$SNAP_NAME" ]]; then
            echo -e "${RED}Fout: Snapshot naam vereist voor delete${NC}"
            usage
        fi

        echo -e "${YELLOW}Snapshot '$SNAP_NAME' verwijderen van VM '$VM'...${NC}"
        virsh snapshot-delete "$VM" "$SNAP_NAME"
        echo -e "${GREEN}Verwijderd!${NC}"
        ;;

    info)
        if [[ -z "$SNAP_NAME" ]]; then
            echo -e "${RED}Fout: Snapshot naam vereist voor info${NC}"
            usage
        fi

        virsh snapshot-info "$VM" "$SNAP_NAME"
        ;;

    *)
        echo -e "${RED}Onbekend commando: $CMD${NC}"
        usage
        ;;
esac

Hoe dat uitpakt

# Voor iets riskants doen
vmsnap create testvm before-experiment

# Doe het riskante ding...
# Oh nee, het is kapot!

# Terug naar veiligheid
vmsnap revert testvm before-experiment

# Of gewoon terug naar de laatste snapshot
vmsnap revert testvm

Het IP vinden: vmip

Dit script bestaat omdat virsh domifaddr me één keer te vaak voorgelogen heeft. Het werkt pas zodra de guest agent draait, wat betekent dat het me vlak na boot, precies wanneer ik het IP wil, niets vertelt. Dus dit script probeert de guest agent, dan de DHCP lease, dan de ARP tabel, en geeft terug wat als eerste antwoordt:

#!/bin/bash
# vmip - Haal VM IP adres(sen) op
# Gebruik: vmip <vm-naam> [-w]

set -euo pipefail

VM="${1:-}"
WAIT="${2:-}"

if [[ -z "$VM" ]]; then
    echo "Gebruik: vmip <vm-naam> [-w]"
    echo "  -w  Wacht tot IP beschikbaar is"
    exit 1
fi

get_ip() {
    # Probeer meerdere methodes

    # Methode 1: Guest agent (meest betrouwbaar als geïnstalleerd)
    IP=$(virsh domifaddr "$VM" --source agent 2>/dev/null | grep -oP '\d+\.\d+\.\d+\.\d+' | head -1)
    [[ -n "$IP" ]] && echo "$IP" && return 0

    # Methode 2: DHCP lease (werkt voor NAT netwerk)
    IP=$(virsh domifaddr "$VM" --source lease 2>/dev/null | grep -oP '\d+\.\d+\.\d+\.\d+' | head -1)
    [[ -n "$IP" ]] && echo "$IP" && return 0

    # Methode 3: ARP tabel
    MAC=$(virsh domiflist "$VM" 2>/dev/null | grep -oP '([0-9a-f]{2}:){5}[0-9a-f]{2}' | head -1)
    if [[ -n "$MAC" ]]; then
        IP=$(arp -an | grep -i "$MAC" | grep -oP '\d+\.\d+\.\d+\.\d+' | head -1)
        [[ -n "$IP" ]] && echo "$IP" && return 0
    fi

    return 1
}

if [[ "$WAIT" == "-w" ]]; then
    echo "Wachten op IP adres voor '$VM'..." >&2
    for i in {1..60}; do
        if IP=$(get_ip); then
            echo "$IP"
            exit 0
        fi
        sleep 2
    done
    echo "Timeout bij wachten op IP" >&2
    exit 1
else
    if IP=$(get_ip); then
        echo "$IP"
    else
        echo "Geen IP gevonden voor '$VM'" >&2
        echo "VM draait misschien niet of IP is nog niet toegewezen" >&2
        exit 1
    fi
fi

Gebruik het zo:

# Krijg IP direct (als beschikbaar)
vmip myvm

# Wacht op IP (handig direct na boot)
vmip myvm -w

# SSH direct
ssh user@$(vmip myvm)

Dingen in bulk doen: vmall

Zodra je een cluster test-VMs draait, wordt ze één voor één bedienen snel vervelend. Vier k8s nodes opspinnen en ze met de hand starten is precies het soort repetitieve klus dat mijn brein weigert betrouwbaar te doen, dus vmall doet het voor me:

#!/bin/bash
# vmall - Voer operatie uit op alle VMs (of gefilterde set)
# Gebruik: vmall <commando> [filter]

set -euo pipefail

CMD="${1:-list}"
FILTER="${2:-}"

case "$CMD" in
    list|ls)
        virsh list --all
        ;;

    start)
        for vm in $(virsh list --name --state-shutoff | grep -E "${FILTER:-.}"); do
            echo "Starten $vm..."
            virsh start "$vm"
        done
        ;;

    stop|shutdown)
        for vm in $(virsh list --name --state-running | grep -E "${FILTER:-.}"); do
            echo "Stoppen $vm..."
            virsh shutdown "$vm"
        done
        ;;

    kill|destroy)
        for vm in $(virsh list --name --state-running | grep -E "${FILTER:-.}"); do
            echo "Forceer stoppen $vm..."
            virsh destroy "$vm"
        done
        ;;

    ips)
        for vm in $(virsh list --name --state-running | grep -E "${FILTER:-.}"); do
            IP=$(vmip "$vm" 2>/dev/null || echo "N/A")
            printf "%-20s %s\n" "$vm" "$IP"
        done
        ;;

    snap)
        SNAP_NAME="bulk-$(date +%Y%m%d-%H%M%S)"
        for vm in $(virsh list --name | grep -E "${FILTER:-.}"); do
            echo "Snapshotting $vm..."
            virsh snapshot-create-as "$vm" "$SNAP_NAME" 2>/dev/null || echo "  Gefaald (externe disk?)"
        done
        ;;

    *)
        echo "Gebruik: vmall <commando> [filter]"
        echo ""
        echo "Commando's:"
        echo "  list              Toon alle VMs"
        echo "  start [filter]    Start gestopte VMs die matchen"
        echo "  stop [filter]     Stop draaiende VMs netjes"
        echo "  kill [filter]     Forceer stop draaiende VMs"
        echo "  ips [filter]      Toon IPs van draaiende VMs"
        echo "  snap [filter]     Snapshot alle VMs"
        echo ""
        echo "Filter is een regex patroon (bijv. 'k8s' voor alle k8s-* VMs)"
        ;;
esac

In de praktijk

# Start alle k8s nodes
vmall start k8s

# Krijg IPs voor alle draaiende VMs
vmall ips

# Snapshot alles voor cluster upgrade
vmall snap

# Stop alle test VMs
vmall stop test

De installer helemaal overslaan: vmcloud

Voor alles dat cloud-init spreekt, en dat zijn de meeste moderne distro’s, is een installer doorlopen verspilde tijd. Een cloud image is al een werkend systeem. Je geeft het een beetje config (hostname, je SSH key, de packages die je wilt) en het boot direct in een bruikbare machine. Dit script regelt dat:

#!/bin/bash
# vmcloud - Maak VM van cloud image met cloud-init
# Gebruik: vmcloud <naam> <cloud-image> [ram-gb] [cpus] [disk-gb]

set -euo pipefail

VM_PATH="/var/lib/libvirt/images"
CLOUD_INIT_PATH="/tmp/cloud-init-$$"

DEFAULT_RAM=2
DEFAULT_CPUS=2
DEFAULT_DISK=20
DEFAULT_USER="admin"
DEFAULT_PASSWORD="changeme"  # Override met env var

NAME="${1:-}"
IMAGE="${2:-}"
RAM="${3:-$DEFAULT_RAM}"
CPUS="${4:-$DEFAULT_CPUS}"
DISK="${5:-$DEFAULT_DISK}"

SSH_KEY="${SSH_KEY:-$(cat ~/.ssh/id_ed25519.pub 2>/dev/null || cat ~/.ssh/id_rsa.pub 2>/dev/null || echo '')}"
PASSWORD="${VM_PASSWORD:-$DEFAULT_PASSWORD}"

usage() {
    echo "Gebruik: vmcloud <naam> <cloud-image> [ram-gb] [cpus] [disk-gb]"
    echo ""
    echo "Environment variabelen:"
    echo "  SSH_KEY       SSH public key om te injecteren (default: ~/.ssh/id_ed25519.pub)"
    echo "  VM_PASSWORD   Wachtwoord voor default user (default: changeme)"
    echo ""
    echo "Voorbeelden:"
    echo "  vmcloud myvm ubuntu-22.04-minimal-cloudimg-amd64.img"
    echo "  vmcloud myvm debian-12-genericcloud-amd64.qcow2 4 4 50"
    exit 1
}

[[ $# -lt 2 ]] && usage

# Maak disk van cloud image
DISK_PATH="${VM_PATH}/${NAME}.qcow2"

if [[ -f "$DISK_PATH" ]]; then
    echo "Fout: Disk bestaat al: $DISK_PATH"
    exit 1
fi

echo "Disk aanmaken van cloud image..."
cp "$IMAGE" "$DISK_PATH"
qemu-img resize "$DISK_PATH" "${DISK}G"

# Maak cloud-init config
mkdir -p "$CLOUD_INIT_PATH"

cat > "$CLOUD_INIT_PATH/meta-data" <<EOF
instance-id: ${NAME}
local-hostname: ${NAME}
EOF

cat > "$CLOUD_INIT_PATH/user-data" <<EOF
#cloud-config
hostname: ${NAME}
manage_etc_hosts: true

users:
  - name: ${DEFAULT_USER}
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    lock_passwd: false
    passwd: $(openssl passwd -6 "$PASSWORD")
    ssh_authorized_keys:
      - ${SSH_KEY}

package_update: true
package_upgrade: false

packages:
  - qemu-guest-agent

runcmd:
  - systemctl enable --now qemu-guest-agent
EOF

# Maak cloud-init ISO
genisoimage -output "$CLOUD_INIT_PATH/cloud-init.iso" \
    -volid cidata -joliet -rock \
    "$CLOUD_INIT_PATH/user-data" \
    "$CLOUD_INIT_PATH/meta-data" 2>/dev/null

CLOUD_INIT_ISO="${VM_PATH}/${NAME}-cloud-init.iso"
mv "$CLOUD_INIT_PATH/cloud-init.iso" "$CLOUD_INIT_ISO"
rm -rf "$CLOUD_INIT_PATH"

# Maak VM
RAM_MB=$((RAM * 1024))

virt-install \
    --name "$NAME" \
    --ram "$RAM_MB" \
    --vcpus "$CPUS" \
    --disk "path=$DISK_PATH,format=qcow2,bus=virtio" \
    --disk "path=$CLOUD_INIT_ISO,device=cdrom" \
    --os-variant linux2022 \
    --network network=default,model=virtio \
    --graphics spice,listen=none \
    --console pty,target_type=serial \
    --import \
    --noautoconsole

echo ""
echo "VM '$NAME' aangemaakt!"
echo ""
echo "Default credentials:"
echo "  User: $DEFAULT_USER"
echo "  Password: $PASSWORD (als SSH key niet werkte)"
echo ""
echo "Verbinden:"
echo "  ssh $DEFAULT_USER@\$(vmip $NAME -w)"

Download de cloud image één keer, en vanaf dat moment is een verse VM een paar seconden weg:

# Download een cloud image eenmalig
wget https://cloud-images.ubuntu.com/minimal/releases/jammy/release/ubuntu-22.04-minimal-cloudimg-amd64.img -O ~/ISOs/ubuntu-cloud.img

# Spin VMs direct op
vmcloud web-server ~/ISOs/ubuntu-cloud.img 2 2 20
vmcloud db-server ~/ISOs/ubuntu-cloud.img 8 4 100

# SSH direct erin
ssh admin@$(vmip web-server -w)

VMs verplaatsen: vmexport en vmimport

Ik schuif VMs vaak genoeg tussen machines dat dit ertoe doet. Een test-VM promoveert van mijn werkstation naar de server. Een VM wordt geback-upt voordat ik iets doe waar ik spijt van kan krijgen. De native virsh commando’s kunnen het allemaal, maar het is een reeks van dumpxml, disks kopiëren, paden fixen, opnieuw definiëren, en ik vergeet elke keer wel een stap. Dus heb ik die hele dans verpakt.

Exporteren: vmexport

#!/bin/bash
# vmexport - Exporteer VM naar draagbaar archief (disk + XML)
# Gebruik: vmexport <vm-naam> [output-dir]

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

NAME="${1:-}"
OUTPUT_DIR="${2:-.}"

usage() {
    echo "Gebruik: vmexport <vm-naam> [output-dir]"
    echo ""
    echo "Exporteert VM definitie en disks naar een directory."
    echo "De VM zou gestopt moeten zijn voor consistente export."
    echo ""
    echo "Voorbeelden:"
    echo "  vmexport myvm                    # Export naar huidige directory"
    echo "  vmexport myvm /backup/vms        # Export naar specifieke directory"
    exit 1
}

# Interactieve selectie met fzf als geen VM opgegeven
if [[ -z "$NAME" ]]; then
    if command -v fzf &>/dev/null; then
        NAME=$(virsh list --all --name | grep -v '^$' | \
            fzf --prompt="Selecteer VM om te exporteren: " \
                --preview 'virsh dominfo {} 2>/dev/null; echo "---"; virsh domblklist {} 2>/dev/null')
        [[ -z "$NAME" ]] && exit 1
    else
        usage
    fi
fi

# Verifieer VM bestaat
if ! virsh dominfo "$NAME" &>/dev/null; then
    echo -e "${RED}Fout: VM '$NAME' niet gevonden${NC}"
    exit 1
fi

# Waarschuw als VM draait
STATE=$(virsh domstate "$NAME" 2>/dev/null)
if [[ "$STATE" != "shut off" ]]; then
    echo -e "${YELLOW}Waarschuwing: VM is $STATE. Voor consistente export, stop hem eerst.${NC}"
    read -p "Toch doorgaan? [j/N] " confirm
    [[ ! "$confirm" =~ ^[JjYy]$ ]] && exit 1
fi

# Maak export directory
EXPORT_DIR="${OUTPUT_DIR}/${NAME}-export-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$EXPORT_DIR"

echo -e "${GREEN}VM exporteren: $NAME${NC}"
echo "  Output: $EXPORT_DIR"
echo ""

# Exporteer XML definitie
echo -e "${CYAN}VM definitie exporteren...${NC}"
virsh dumpxml "$NAME" > "$EXPORT_DIR/${NAME}.xml"

# Exporteer elke disk
echo -e "${CYAN}Disks exporteren...${NC}"
while IFS= read -r line; do
    TARGET=$(echo "$line" | awk '{print $1}')
    SOURCE=$(echo "$line" | awk '{print $2}')

    # Skip lege regels en CDROMs
    [[ -z "$SOURCE" || "$SOURCE" == "-" ]] && continue
    [[ ! -f "$SOURCE" ]] && continue

    DISK_NAME=$(basename "$SOURCE")
    echo "  $TARGET: $SOURCE -> $DISK_NAME"

    # Gebruik qemu-img convert om te comprimeren
    qemu-img convert -O qcow2 -c "$SOURCE" "$EXPORT_DIR/$DISK_NAME"

done < <(virsh domblklist "$NAME" --details | grep -E "file\s+disk" | awk '{print $3, $4}')

# Maak metadata bestand
cat > "$EXPORT_DIR/metadata.txt" <<EOF
VM Export
=========
Naam: $NAME
Datum: $(date)
Bron host: $(hostname)
Originele staat: $STATE

Bestanden:
$(ls -lh "$EXPORT_DIR")
EOF

# Bereken totale grootte
TOTAL_SIZE=$(du -sh "$EXPORT_DIR" | cut -f1)

echo ""
echo -e "${GREEN}Export compleet!${NC}"
echo "  Locatie: $EXPORT_DIR"
echo "  Grootte: $TOTAL_SIZE"
echo ""
echo "Om te importeren op andere host:"
echo "  scp -r $EXPORT_DIR user@target:/path/"
echo "  vmimport /path/$(basename "$EXPORT_DIR")"

Importeren: vmimport

#!/bin/bash
# vmimport - Importeer VM van geëxporteerd archief
# Gebruik: vmimport <export-dir> [nieuwe-naam]

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

VM_PATH="/var/lib/libvirt/images"
EXPORT_DIR="${1:-}"
NEW_NAME="${2:-}"

usage() {
    echo "Gebruik: vmimport <export-dir> [nieuwe-naam]"
    echo ""
    echo "Importeert een VM van vmexport archief."
    echo ""
    echo "Voorbeelden:"
    echo "  vmimport ./myvm-export-20240101-120000"
    echo "  vmimport ./myvm-export-20240101-120000 myvm-restored"
    exit 1
}

[[ -z "$EXPORT_DIR" ]] && usage
[[ ! -d "$EXPORT_DIR" ]] && { echo -e "${RED}Fout: Directory niet gevonden: $EXPORT_DIR${NC}"; exit 1; }

# Vind het XML bestand
XML_FILE=$(find "$EXPORT_DIR" -name "*.xml" -type f | head -1)
[[ -z "$XML_FILE" ]] && { echo -e "${RED}Fout: Geen XML definitie gevonden in $EXPORT_DIR${NC}"; exit 1; }

# Haal originele naam uit XML
ORIG_NAME=$(grep -oP "(?<=<name>)[^<]+" "$XML_FILE")
NAME="${NEW_NAME:-$ORIG_NAME}"

echo -e "${GREEN}VM importeren: $NAME${NC}"
echo "  Bron: $EXPORT_DIR"
echo "  Originele naam: $ORIG_NAME"
[[ -n "$NEW_NAME" ]] && echo "  Nieuwe naam: $NEW_NAME"
echo ""

# Check of VM al bestaat
if virsh dominfo "$NAME" &>/dev/null; then
    echo -e "${RED}Fout: VM '$NAME' bestaat al${NC}"
    echo "Gebruik een andere naam: vmimport $EXPORT_DIR andere-naam"
    exit 1
fi

# Kopieer disks naar libvirt images directory
echo -e "${CYAN}Disks kopiëren...${NC}"
declare -A DISK_MAP

for disk in "$EXPORT_DIR"/*.qcow2 "$EXPORT_DIR"/*.img; do
    [[ ! -f "$disk" ]] && continue

    DISK_NAME=$(basename "$disk")

    # Als VM hernoemt, hernoem ook disk bestanden
    if [[ -n "$NEW_NAME" ]]; then
        NEW_DISK_NAME="${DISK_NAME/$ORIG_NAME/$NEW_NAME}"
    else
        NEW_DISK_NAME="$DISK_NAME"
    fi

    DEST_PATH="$VM_PATH/$NEW_DISK_NAME"

    # Check of bestemming bestaat
    if [[ -f "$DEST_PATH" ]]; then
        echo -e "${YELLOW}Waarschuwing: $DEST_PATH bestaat al${NC}"
        read -p "Overschrijven? [j/N] " confirm
        [[ ! "$confirm" =~ ^[JjYy]$ ]] && { echo "Overslaan..."; continue; }
    fi

    echo "  $DISK_NAME -> $DEST_PATH"
    cp "$disk" "$DEST_PATH"

    # Sla mapping op voor XML update
    DISK_MAP["$DISK_NAME"]="$DEST_PATH"
done

# Bereid XML voor met bijgewerkte paden en naam
echo -e "${CYAN}VM definitie voorbereiden...${NC}"
TEMP_XML=$(mktemp)
cp "$XML_FILE" "$TEMP_XML"

# Update VM naam als gewijzigd
if [[ -n "$NEW_NAME" ]]; then
    sed -i "s|<name>$ORIG_NAME</name>|<name>$NEW_NAME</name>|g" "$TEMP_XML"
fi

# Update disk paden
for orig_disk in "${!DISK_MAP[@]}"; do
    new_path="${DISK_MAP[$orig_disk]}"
    sed -i "s|file='[^']*${orig_disk}'|file='${new_path}'|g" "$TEMP_XML"
    sed -i "s|source file=\"[^\"]*${orig_disk}\"|source file=\"${new_path}\"|g" "$TEMP_XML"
done

# Verwijder UUID (laat libvirt nieuwe genereren)
sed -i '/<uuid>/d' "$TEMP_XML"

# Verwijder MAC adressen (laat libvirt nieuwe genereren om conflicten te voorkomen)
sed -i '/<mac address=/d' "$TEMP_XML"

# Definieer de VM
echo -e "${CYAN}VM definiëren...${NC}"
virsh define "$TEMP_XML"
rm "$TEMP_XML"

echo ""
echo -e "${GREEN}Import compleet!${NC}"
echo ""
echo "Start de VM met: vmstart $NAME"

Live migreren: vmmigrate

Soms moet de VM verhuizen terwijl hij nog draait, of in elk geval zonder dat ik de export/import shuffle hoef te babysitten. Dit script vangt de drie gevallen af die ik echt tegenkom: een echte live migratie als er shared storage is, een live migratie die de disk meesleept als die er niet is, en een gewone offline verhuizing die stopt, kopieert en aan de andere kant herstart:

#!/bin/bash
# vmmigrate - Migreer VM naar andere libvirt host
# Gebruik: vmmigrate <vm-naam> <doel-host> [--live] [--offline]

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

NAME="${1:-}"
TARGET="${2:-}"
MODE="${3:---offline}"

# Bekende hosts voor fzf selectie (pas dit aan)
KNOWN_HOSTS="${VM_MIGRATE_HOSTS:-}"

usage() {
    echo "Gebruik: vmmigrate <vm-naam> <doel-host> [--live|--offline]"
    echo ""
    echo "Migreer een VM naar een andere libvirt host."
    echo ""
    echo "Opties:"
    echo "  --live      Live migratie (VM blijft draaien, vereist shared storage"
    echo "              of zelfde CPU model)"
    echo "  --offline   Offline migratie (default, VM wordt gestopt, disk gekopieerd)"
    echo "  --copy      Live migratie met disk kopie (langzamer maar geen shared storage nodig)"
    echo ""
    echo "Voorbeelden:"
    echo "  vmmigrate myvm server2 --offline     # Stop, kopieer, start op server2"
    echo "  vmmigrate myvm server2 --live        # Live migratie (shared storage)"
    echo "  vmmigrate myvm server2 --copy        # Live migratie met disk kopie"
    echo ""
    echo "Environment:"
    echo "  VM_MIGRATE_HOSTS   Spatie-gescheiden lijst van hosts voor fzf selectie"
    exit 1
}

# Interactieve VM selectie
if [[ -z "$NAME" ]] && command -v fzf &>/dev/null; then
    NAME=$(virsh list --all --name | grep -v '^$' | \
        fzf --prompt="Selecteer VM om te migreren: " \
            --preview 'virsh dominfo {} 2>/dev/null')
    [[ -z "$NAME" ]] && exit 1
fi

# Interactieve host selectie
if [[ -z "$TARGET" ]] && command -v fzf &>/dev/null; then
    if [[ -n "$KNOWN_HOSTS" ]]; then
        TARGET=$(echo "$KNOWN_HOSTS" | tr ' ' '\n' | \
            fzf --prompt="Selecteer doel host: " \
                --preview 'ssh {} "virsh list --all" 2>/dev/null || echo "Kan niet verbinden"')
    else
        read -p "Doel host: " TARGET
    fi
    [[ -z "$TARGET" ]] && exit 1
fi

[[ -z "$NAME" || -z "$TARGET" ]] && usage

# Verifieer VM bestaat
if ! virsh dominfo "$NAME" &>/dev/null; then
    echo -e "${RED}Fout: VM '$NAME' niet gevonden${NC}"
    exit 1
fi

# Test SSH connectiviteit
echo -e "${CYAN}Verbinding testen naar $TARGET...${NC}"
if ! ssh -o ConnectTimeout=5 "$TARGET" "virsh version" &>/dev/null; then
    echo -e "${RED}Fout: Kan niet verbinden met libvirt op $TARGET${NC}"
    echo "Zorg dat SSH werkt en libvirt draait: ssh $TARGET 'virsh version'"
    exit 1
fi

STATE=$(virsh domstate "$NAME" 2>/dev/null)
echo -e "${GREEN}VM migreren: $NAME${NC}"
echo "  Van: $(hostname)"
echo "  Naar: $TARGET"
echo "  Modus: $MODE"
echo "  Huidige staat: $STATE"
echo ""

case "$MODE" in
    --live)
        if [[ "$STATE" != "running" ]]; then
            echo -e "${RED}Fout: Live migratie vereist dat VM draait${NC}"
            exit 1
        fi

        echo -e "${CYAN}Live migratie starten...${NC}"
        echo "Dit vereist shared storage tussen hosts."

        virsh migrate --live --persistent --undefinesource \
            "$NAME" "qemu+ssh://$TARGET/system"

        echo -e "${GREEN}Live migratie compleet!${NC}"
        ;;

    --copy)
        if [[ "$STATE" != "running" ]]; then
            echo -e "${RED}Fout: Copy migratie vereist dat VM draait${NC}"
            exit 1
        fi

        echo -e "${CYAN}Live migratie met disk kopie starten...${NC}"
        echo "Dit kopieert alle disks over het netwerk (kan traag zijn voor grote disks)."

        virsh migrate --live --persistent --undefinesource \
            --copy-storage-all \
            "$NAME" "qemu+ssh://$TARGET/system"

        echo -e "${GREEN}Migratie met disk kopie compleet!${NC}"
        ;;

    --offline|*)
        echo -e "${CYAN}Offline migratie starten...${NC}"

        # Stop als draaiend
        if [[ "$STATE" == "running" ]]; then
            echo "VM stoppen..."
            virsh shutdown "$NAME"

            # Wacht op shutdown
            for i in {1..60}; do
                STATE=$(virsh domstate "$NAME" 2>/dev/null)
                [[ "$STATE" == "shut off" ]] && break
                sleep 2
            done

            if [[ "$STATE" != "shut off" ]]; then
                echo -e "${YELLOW}VM stopte niet netjes, forceren...${NC}"
                virsh destroy "$NAME"
            fi
        fi

        # Exporteer
        TEMP_DIR=$(mktemp -d)
        echo "Exporteren naar $TEMP_DIR..."

        virsh dumpxml "$NAME" > "$TEMP_DIR/${NAME}.xml"

        # Kopieer disks
        while IFS= read -r line; do
            SOURCE=$(echo "$line" | awk '{print $2}')
            [[ -z "$SOURCE" || "$SOURCE" == "-" || ! -f "$SOURCE" ]] && continue

            DISK_NAME=$(basename "$SOURCE")
            echo "  Kopiëren $DISK_NAME..."
            cp "$SOURCE" "$TEMP_DIR/"
        done < <(virsh domblklist "$NAME" --details | grep -E "file\s+disk" | awk '{print $3, $4}')

        # Transfer naar remote
        echo "Overdragen naar $TARGET..."
        REMOTE_PATH="/var/lib/libvirt/images"

        # Kopieer disks eerst
        for disk in "$TEMP_DIR"/*.qcow2 "$TEMP_DIR"/*.img; do
            [[ ! -f "$disk" ]] && continue
            scp "$disk" "$TARGET:$REMOTE_PATH/"
        done

        # Update XML met nieuwe paden en kopieer
        sed -i "s|/var/lib/libvirt/images/|$REMOTE_PATH/|g" "$TEMP_DIR/${NAME}.xml"
        sed -i '/<uuid>/d' "$TEMP_DIR/${NAME}.xml"
        scp "$TEMP_DIR/${NAME}.xml" "$TARGET:/tmp/"

        # Definieer op remote
        ssh "$TARGET" "virsh define /tmp/${NAME}.xml && rm /tmp/${NAME}.xml"

        # Ruim lokaal op
        rm -rf "$TEMP_DIR"

        # Optioneel undefine lokaal
        read -p "VM verwijderen van deze host? [j/N] " confirm
        if [[ "$confirm" =~ ^[JjYy]$ ]]; then
            virsh undefine "$NAME" --remove-all-storage
            echo "VM verwijderd van lokale host."
        fi

        echo ""
        echo -e "${GREEN}Offline migratie compleet!${NC}"
        echo "Start de VM op $TARGET met: ssh $TARGET 'vmstart $NAME'"
        ;;
esac

Clonen: vmclone

Als ik een wegwerpkopie van een bekend-goede machine wil, wint clonen het van vanaf nul bouwen. Houd een schone ubuntu-base bij de hand, clone hem, en de kopie is in seconden klaar:

#!/bin/bash
# vmclone - Clone een bestaande VM
# Gebruik: vmclone <bron-vm> <nieuwe-naam>

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

SOURCE="${1:-}"
NEW_NAME="${2:-}"

usage() {
    echo "Gebruik: vmclone <bron-vm> <nieuwe-naam>"
    echo ""
    echo "Maakt een volledige clone van een bestaande VM."
    echo "Bron VM zou gestopt moeten zijn voor consistente clone."
    echo ""
    echo "Voorbeelden:"
    echo "  vmclone ubuntu-base ubuntu-test"
    echo "  vmclone k8s-template k8s-worker-3"
    exit 1
}

# Interactieve selectie met fzf
if [[ -z "$SOURCE" ]] && command -v fzf &>/dev/null; then
    SOURCE=$(virsh list --all --name | grep -v '^$' | \
        fzf --prompt="Selecteer VM om te clonen: " \
            --preview 'virsh dominfo {} 2>/dev/null')
    [[ -z "$SOURCE" ]] && exit 1
fi

if [[ -z "$NEW_NAME" ]]; then
    read -p "Nieuwe VM naam: " NEW_NAME
    [[ -z "$NEW_NAME" ]] && { echo "Naam vereist"; exit 1; }
fi

[[ -z "$SOURCE" || -z "$NEW_NAME" ]] && usage

# Verifieer bron bestaat
if ! virsh dominfo "$SOURCE" &>/dev/null; then
    echo -e "${RED}Fout: Bron VM '$SOURCE' niet gevonden${NC}"
    exit 1
fi

# Check of doel al bestaat
if virsh dominfo "$NEW_NAME" &>/dev/null; then
    echo -e "${RED}Fout: VM '$NEW_NAME' bestaat al${NC}"
    exit 1
fi

# Waarschuw als bron draait
STATE=$(virsh domstate "$SOURCE" 2>/dev/null)
if [[ "$STATE" != "shut off" ]]; then
    echo -e "${YELLOW}Waarschuwing: Bron VM is $STATE.${NC}"
    echo "Clone kan inconsistent zijn. Stop bron voor beste resultaten."
    read -p "Toch doorgaan? [j/N] " confirm
    [[ ! "$confirm" =~ ^[JjYy]$ ]] && exit 1
fi

echo -e "${GREEN}VM clonen: $SOURCE -> $NEW_NAME${NC}"
echo ""

# Gebruik virt-clone voor het zware werk
virt-clone \
    --original "$SOURCE" \
    --name "$NEW_NAME" \
    --auto-clone

echo ""
echo -e "${GREEN}Clone compleet!${NC}"
echo ""
echo "Start de clone met: vmstart $NEW_NAME"

# Toon disk locaties
echo ""
echo "Clone disks:"
virsh domblklist "$NEW_NAME" --details | grep -E "file\s+disk" | awk '{print "  " $4}'

De migratie-tools samen

# Clone een template voor testen
vmclone ubuntu-base test-ubuntu
vmstart test-ubuntu

# Exporteer VM voor backup
vmexport important-vm /backup/vms/

# Verplaats VM naar andere server (offline)
vmmigrate webserver server2.local --offline

# Live migratie met shared storage (NFS/Ceph)
vmmigrate database server2.local --live

# Verplaats VM naar server zonder shared storage
vmmigrate webserver server2.local --copy

# Importeer een VM van backup
vmimport /backup/vms/important-vm-export-20240101/
vmimport /backup/vms/important-vm-export-20240101/ restored-vm  # Met nieuwe naam

Interactief met fzf

Als je fzf bij de hand hebt, wordt het hele geheel een stuk prettiger. De helft van de frictie bij virsh is exacte VM namen onthouden, en fzf maakt daar korte metten mee: typ een paar letters, zie een live preview van de VM, druk op enter. Deze functies wikkelen de scripts hierboven met die selector:

# ~/.vm-aliases - fzf enhanced sectie

# Interactieve VM selector (gebruik in andere scripts)
vmf() {
    virsh list --all --name | grep -v '^$' | \
        fzf --prompt="Selecteer VM: " \
            --preview 'virsh dominfo {} 2>/dev/null; echo "---"; virsh domifaddr {} 2>/dev/null' \
            --preview-window=right:50%
}

# SSH naar VM met fuzzy selectie
vmssh() {
    local user="${1:-root}"
    local vm=$(virsh list --name | grep -v '^$' | \
        fzf --prompt="SSH naar VM: " \
            --preview 'echo "IP: $(vmip {} 2>/dev/null || echo "N/A")"; echo "---"; virsh dominfo {} 2>/dev/null | grep -E "State|CPU|memory"')
    [[ -z "$vm" ]] && return 1
    local ip=$(vmip "$vm" 2>/dev/null)
    [[ -z "$ip" ]] && { echo "Geen IP voor $vm"; return 1; }
    ssh "${user}@${ip}"
}

# Start VMs met multi-select
vmstartf() {
    virsh list --name --state-shutoff | grep -v '^$' | \
        fzf --multi --prompt="Start VMs (TAB om meerdere te selecteren): " \
            --preview 'virsh dominfo {} 2>/dev/null' | \
        xargs -I{} virsh start {}
}

# Stop VMs met multi-select
vmstopf() {
    virsh list --name --state-running | grep -v '^$' | \
        fzf --multi --prompt="Stop VMs (TAB om meerdere te selecteren): " \
            --preview 'virsh dominfo {} 2>/dev/null; echo "---"; echo "IP: $(vmip {} 2>/dev/null)"' | \
        xargs -I{} virsh shutdown {}
}

# Verwijder VM met veiligheids preview
vmrmf() {
    local vm=$(virsh list --all --name | grep -v '^$' | \
        fzf --prompt="VERWIJDER VM (voorzichtig!): " \
            --preview 'echo "=== VM Info ==="; virsh dominfo {} 2>/dev/null; echo; echo "=== Disks (WORDEN VERWIJDERD) ==="; virsh domblklist {} 2>/dev/null' \
            --preview-window=right:60% \
            --header="WAARSCHUWING: Dit verwijdert de VM en ALLE storage")
    [[ -z "$vm" ]] && return 0
    echo "Verwijder '$vm' en alle storage? [j/N]"
    read -r confirm
    [[ "$confirm" =~ ^[JjYy]$ ]] && virsh undefine "$vm" --remove-all-storage
}

# Snapshot beheer met fzf
vmsnapf() {
    local vm=$(vmf)
    [[ -z "$vm" ]] && return 1

    local action=$(echo -e "create\nlist\nrevert\ndelete" | fzf --prompt="Snapshot actie voor $vm: ")

    case "$action" in
        create)
            read -p "Snapshot naam (leeg voor timestamp): " name
            vmsnap create "$vm" "$name"
            ;;
        list)
            vmsnap list "$vm"
            ;;
        revert)
            local snap=$(virsh snapshot-list "$vm" --name 2>/dev/null | grep -v '^$' | \
                fzf --prompt="Terug naar snapshot: " \
                    --preview "virsh snapshot-info $vm {} 2>/dev/null" \
                    --tac)
            [[ -n "$snap" ]] && vmsnap revert "$vm" "$snap"
            ;;
        delete)
            local snap=$(virsh snapshot-list "$vm" --name 2>/dev/null | grep -v '^$' | \
                fzf --prompt="Verwijder snapshot: " \
                    --preview "virsh snapshot-info $vm {} 2>/dev/null")
            [[ -n "$snap" ]] && vmsnap delete "$vm" "$snap"
            ;;
    esac
}

# Snelle VM van ISO met fzf selectie
vmquickf() {
    local iso=$(find ~/Downloads ~/ISOs /var/lib/libvirt/images -maxdepth 2 \
        \( -name "*.iso" -o -name "*.img" \) 2>/dev/null | \
        fzf --prompt="Selecteer ISO/image: " \
            --preview 'ls -lh {}; file {}')
    [[ -z "$iso" ]] && return 1

    local suggested=$(basename "$iso" | sed 's/\.[^.]*$//' | tr '[:upper:]' '[:lower:]' | tr ' _' '-' | cut -c1-20)
    read -p "VM naam [$suggested]: " name
    name="${name:-$suggested}"

    read -p "RAM in GB [2]: " ram
    read -p "CPUs [2]: " cpus
    read -p "Disk in GB [20]: " disk

    vmquick "$name" "$iso" "${ram:-2}" "${cpus:-2}" "${disk:-20}"
}

Met deze typ ik echt nooit meer een VM naam helemaal uit. Een paar letters en een preview is alles wat het kost.

Installatie

Alle scripts zijn beschikbaar op GitHub: github.com/kapott/vm-scripts

# Clone en installeer
git clone https://github.com/kapott/vm-scripts.git
cd vm-scripts
./install.sh

Of met de hand: zet de scripts in ~/.local/bin/ en zorg dat die op je PATH staat:

# In .bashrc of .zshrc
export PATH="$HOME/.local/bin:$PATH"

# Source de aliases
source ~/.local/share/vm-scripts/vm-aliases.sh

Het complete aliases-bestand

Hier is het volledige .vm-aliases bestand op één plek, klaar om te sourcen:

# ~/.vm-aliases
# Source dit in .bashrc: source ~/.vm-aliases

# === Status ===
alias vms='virsh list --all'
alias vmsr='virsh list'
alias vmnets='virsh net-list --all'
alias vmpools='virsh pool-list --all'

# === Power ===
alias vmstart='virsh start'
alias vmstop='virsh shutdown'
alias vmkill='virsh destroy'
alias vmreboot='virsh reboot'
alias vmsuspend='virsh suspend'
alias vmresume='virsh resume'

# === Console ===
alias vmcon='virsh console'
alias vmview='virt-viewer'

# === Info ===
alias vminfo='virsh dominfo'
alias vmblk='virsh domblklist'
alias vmnet='virsh domiflist'

# === Management ===
alias vmedit='virsh edit'
alias vmxml='virsh dumpxml'
alias vmrm='virsh undefine --remove-all-storage'

# === Completions (bash) ===
_vm_complete() {
    local cur="${COMP_WORDS[COMP_CWORD]}"
    COMPREPLY=($(compgen -W "$(virsh list --all --name 2>/dev/null)" -- "$cur"))
}

complete -F _vm_complete vmstart vmstop vmkill vmreboot vmcon vmview vminfo vmblk vmnet vmedit vmxml vmrm vmip vmsnap vmupgrade vmsuspend vmresume

Waarom dit de moeite waard is

Terug naar de vraag waar ik mee begon: hoe maak je een krachtige tool zo wrijvingsloos dat je er ook echt naar grijpt? Het eerlijke antwoord is dat de frictie nooit over KVM zat. KVM is geweldig. De frictie zat in de afstand tussen een VM willen en er een hebben, en deze scripts brengen die afstand terug naar zo’n tien seconden.

Dat klinkt klein tot je ziet wat het met je gedrag doet. Als een schone VM opspinnen echt sneller is dan koffie zetten, stop je met dingen in productie testen omdat je geen zin had ze te isoleren. Je stopt met de test helemaal overslaan. Je stopt met naar een container grijpen terwijl een echte VM de juiste keuze was en je die alleen uit luiheid vermeed. De tool werd geen klus meer, dus gebruik ik hem constant, en mijn werk werd bijna per ongeluk veiliger.

Er zit ook een soevereiniteitskant aan, het soort waar ik in Sovereign Infrastructure steeds op terugkom. Dit zijn een paar honderd regels shell. Ik kan ze allemaal lezen, veranderen wat me niet bevalt, en repareren als ze breken, want niets hierin verbergt zich voor mij. Dat is de ruil die ik blijf maken: wat moeite vooraf met het schrijven van de scripts, in ruil voor tooling die ik volledig bezit en begrijp. virt-manager was sneller te adopteren geweest en trager om mee te leven.

De scripts staan op GitHub, hierboven gelinkt. Pak ze, sloop ze, hernoem alles naar je eigen muscle memory. De specifieke commando’s doen er minder toe dan het principe: als een tool het gebruiken waard is, is hij de kleine moeite waard om het gebruiken moeiteloos te maken.