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.
