· · 29 min leestijd Intermediate

KVM/QEMU scripts en aliases: maak virtualisatie echt bruikbaar

Praktische shell scripts en aliases voor dagelijks KVM/QEMU/libvirt gebruik. Snelle VM creatie, snapshots, en beheer zonder cryptische commando's te onthouden.

Zij start een machine in een pot met één hendel terwijl een monnik-wezen een rol van vijftig regels opzegt voor één

Ik draai de hele dag VMs. Een Kubernetes-deployment die ik wil testen, een distro waar ik over las, Windows voor die ene koppige applicatie, een experiment dat ik liever niet op m’n eigen machine loslaat. Daar is KVM/QEMU gewoon de juiste tool voor. Het zit in de Linux-kernel en draait bijna op native snelheid. Niks erin belt naar huis, en geen vendor die bepaalt wat ik met mijn eigen hardware mag.

Alleen die commando’s.

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 z’n hoofd. Je kopieert het van een wiki, verwisselt een paar waardes, hebt het disk-pad fout, en probeert het nog eens. Tegen de tijd dat de VM boot, is de nieuwsgierigheid waarmee je begon alweer weg. Ik ben de tel kwijt van de experimenten die ik heb overgeslagen omdat een VM optuigen langer duurde dan ik zin had te wachten.

Dus op een gegeven moment ben ik gestopt met vechten en heb ik een stapeltje scripts en aliases geschreven. Een VM vanaf een ISO met defaults waar ik niet over hoef na te denken, en een snapshot die één woord kost. De lat lag laag. Kan ik het sneller typen dan door virt-manager klikken, dan gebruik ik het. Alles hieronder haalt die lat.

Scripts in plaats van de GUI

Op dit soort tooling leun ik om redenen die ik in Working with an AuDHD Brain heb uitgewerkt. Een GUI verstopt alles achter een layout die ik elke keer opnieuw in m’n hoofd moet laden. Waar zat die snapshot-knop ook alweer? Welk tabblad had de disk-instellingen? Dat zoeken kost focus die ik liever besteed aan het probleem waarvoor ik die VM überhaupt opende.

Een script vraagt me niks te onthouden. Het draait elke keer hetzelfde, en het bestand is z’n eigen documentatie. Tab completion wint van elke muis, en ik pipe het ene script in het andere zonder erbij na te denken.

En de saaie winst. Scripts werken over SSH. De meeste van mijn VMs leven op een headless bak in de hoek, en daar heb je aan virt-manager niet veel.

Begin met de aliases

De aliases zijn de goedkoopste 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 is virsh list --all gewoon vms. Op zichzelf stelt dat niets voor. Maar ik draai zo’n handvol tientallen keren per dag, en de toetsaanslagen doen er minder toe dan het “wat was die flag ook alweer”-opzoeken dat ik niet meer doe. Het veranderde hoe vaak ik überhaupt een VM pak.

Het werkpaard: vmquick

Aliases verkorten dingen die je al kent. De echte tijd ging zitten in het aanmaken, dus dit is het script dat ik het meest gebruik. Een VM vanaf een ISO met zo min mogelijk typwerk:

#!/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 erop, 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 zoekt de ISO op de gebruikelijke plekken, dus staat het bestand in ~/Downloads of ~/ISOs, dan volstaat de bestandsnaam en sla je het hele pad over.

Later opschalen: vmupgrade

Elke VM start ik bewust klein. 2GB en twee cores is ruim genoeg om uit te vinden of iets werkt, en heb ik later meer nodig, dan is 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 wat me toestaat om met opzet roekeloos te doen. Sloop de VM, zet terug, probeer opnieuw. De kale virsh snapshot-* commando’s zijn prima maar omslachtig, dus deze wrapper maakt ze kort:

#!/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 heeft voorgelogen. Het antwoordt pas als de guest agent draait, dus vlak na de boot, precies als ik dat IP wil, zegt het niks. 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 alles per VM doen snel vervelend. Vier k8s nodes met de hand starten is precies het soort herhaalwerk dat m’n 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 wat 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 hostname, je SSH-key en een lijstje packages, en het boot meteen als 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 dan is een verse VM een paar seconden werk:

# 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 verhuis VMs vaak genoeg tussen machines dat dit ertoe doet. Een test-VM promoveert van mijn werkstation naar de server, of krijgt een backup voordat ik iets doe waar ik spijt van kan krijgen. virsh kan het allemaal, maar het is dumpxml, disks kopiëren, paden fixen, opnieuw definiëren, en ik vergeet elke keer wel een stap. Dus heb ik die hele reeks ingepakt.

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 die export/import-shuffle hoef te babysitten. Dit script vangt de drie gevallen af die ik 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 weer start:

#!/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

Wil ik een wegwerpkopie van een machine waarvan ik weet dat hij goed is, dan wint clonen het van opnieuw 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

Heb je fzf bij de hand, dan wordt het hele geheel een stuk prettiger. De helft van het gedoe met virsh is exacte VM-namen onthouden. Met fzf typ je een paar letters, zie je een live preview van de VM, en druk je op enter. Deze functies wikkelen de scripts hierboven in 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}"
}

Sindsdien heb ik geen VM-naam meer helemaal uitgetypt. Een paar letters en een preview.

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

Wat tien seconden veranderde

Terug naar die virt-install-regel bovenaan. KVM zelf was al die tijd prima. Wat mij opbrak was de afstand tussen een VM willen en er een hebben, en deze scripts brengen die terug tot zo’n tien seconden typen.

Tien seconden veranderde meer aan mijn gewoontes dan ik had verwacht. Als een schone VM sneller is dan koffie zetten, stop je met dingen in productie testen omdat isoleren als werk voelt. Je pakt geen container meer als een VM de juiste keuze was. De test die je had overgeslagen draait gewoon. Mijn werk werd veiliger, en dat had ik nooit zo gepland.

Er zit ook een soevereiniteitskant aan, die waar ik in Sovereign Infrastructure steeds op terugkom. Dit zijn een paar honderd regels shell. Ik kan ze allemaal lezen, en gaat er een stuk, dan maak ik hem, want niets hierin is voor mij verborgen. Dat kostte me vooraf wat tijd. Het leverde tooling op die ik bezit en snap. virt-manager had ik sneller in gebruik gehad, en dan zat ik nu nog door tabbladen te klikken.

De scripts staan op GitHub, hierboven gelinkt. Pak ze, sloop ze, hernoem alles naar je eigen muscle memory. Dat heb ik zelf ook gedaan met de commando’s die ik ooit van een wiki plukte.