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.
