TL;DR:
La machine de mon homelab qui me sert à faire des tests sur les modèles IA est équipée d'un GPU Nvidia RTX 5090, ce qui accessoirement peut être sympa pour faire du jeu vidéo.
J'ai donc déployé dessus une VM Windows 11 ; cet article décrit la procédure pour le faire (avec en plus quelques améliorations bien utiles).
L'ambition de cet article est de synthétiser dans un exemple réel, complet et validé (et up2date en 07/2026), toutes les informations sur le sujet dispersées entre Reddit, des billets de blog et la documentation de QEMU, afin de proposer un tuto complet (qui, me semble-t-il, ne peut pas être sorti par ChatGPT en trois questions).
Concepts:
Virtualisation, IOMMU, Passthrough, GPU
Outils:
Ubuntu Server, KVM/libvirt (QEMU), Windows 11
Contexte, pré-requis & préparation
Gestion d'une copie temporaire
Gestion du backup à la demande
Gestion des backups automatiques
Utiliser une VM pour le gaming a de nombreux avantages:
Dans l'article qui suit, on ne va pas se priver de mettre en oeuvre toutes ces belles choses !
Pour être honnête, il y a aussi quelques inconvénients.
<mode bonnefoi on>
Liste des devices en passthrough:
# lspci -nn |grep NVIDIA |grep "\[....:....\]"
01:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2b85] (rev a1)
01:00.1 Audio device [0403]: NVIDIA Corporation Device [10de:22e8] (rev a1)
==> Donc 10de:2b85 et 10de:22e8 sont les identifiants correspondant au modèle de la carte graphique présente, et 01:00.0 et 01:00.1 sont respectivement leur adresse sur le bus PCI.
Il semble que les GPU Nvidia récents exposent toujours deux devices sur le bus PCI, correspondant à la carte d'affichage et au contrôleur audio HDMI/DisplayPort (présent ici car le HDMI transporte l'audio en plus de la vidéo).
# lspci -nn |grep ASMedia |grep "\[....:....\]"
16:00.0 PCI bridge [0604]: ASMedia Technology Inc. Device [1b21:2421] (rev 01)
17:00.0 PCI bridge [0604]: ASMedia Technology Inc. Device [1b21:2423] (rev 01)
17:01.0 PCI bridge [0604]: ASMedia Technology Inc. Device [1b21:2423] (rev 01)
17:02.0 PCI bridge [0604]: ASMedia Technology Inc. Device [1b21:2423] (rev 01)
17:03.0 PCI bridge [0604]: ASMedia Technology Inc. Device [1b21:2423] (rev 01)
1a:00.0 USB controller [0c03]: ASMedia Technology Inc. Device [1b21:2426] (rev 01)
1b:00.0 USB controller [0c03]: ASMedia Technology Inc. Device [1b21:2425] (rev 01)
A ce stade et sans plus de connaissance, on est supposé passer à la VM les deux devices USB controller.
Il ne faut jamais passer à une VM un bridge PCI, qui reste géré par l'hôte si l'IOMMU permet d'isoler le contrôleur USB (comme c'est le cas ici).
Dans le cas précis exposé ici, le device 1b21:2425 correspond à un routeur Thunderbolt que Windows ne sait pas gérer, mais pour le savoir j'ai d'abord mappé les deux devices USB controller ASMedia (1b21:2425 et 1b21:2426) pour constater que le 1b21:2425 ne servait à rien, et que donc en passant uniquement le 1b21:2426 ça fonctionne correctement. J'espère que c'est clair.
==> Donc le 1b21:2426 est le device à passer à la VM pour le contrôleur USB4.
On passe donc à la VM un contrôleur USB entier avec ses ports physiques.
Il existe une autre possibilité avec QEMU/KVM: on peut aussi passer des devices USB un par un à une VM, afin de lui mapper un clavier, un souris, un joystick ou n'importe quoi d'autre.
Donc je vais être très clair: j'ai essayé il y a longtemps, ça ne marchait pas bien. Peut-être qu'aujourd'hui c'est bien au point, mais il reste que c'est beaucoup moins pratique à gérer: si un device USB est débranché puis rebranché, ou bien changé de port, il faut repasser dans la conf de l'hyperviseur pour lui dire de le donner à la VM. Impensable pour moi.
S'aider du script suivant pour vérifier les groupes IOMMU et s'assurer que les devices physiques à passer à la VM sont bien séparés:
# vi iommu.sh
#!/bin/bash
for d in /sys/kernel/iommu_groups/*/devices/*; do
n=${d#*/iommu_groups/*}; n=${n%%/*}
printf 'IOMMU Group %s ' "$n"
lspci -nns "${d##*/}"
done
Exemple de résultat (seulement la partie qui nous intéresse):
# ./iommu.sh |grep -Ei "nvidia|asmedia" |grep -v "PCI bridge" |sort
IOMMU Group 15 01:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2b85] (rev a1)
IOMMU Group 15 01:00.1 Audio device [0403]: NVIDIA Corporation Device [10de:22e8] (rev a1)
IOMMU Group 31 1a:00.0 USB controller [0c03]: ASMedia Technology Inc. Device [1b21:2426] (rev 01)
IOMMU Group 32 1b:00.0 USB controller [0c03]: ASMedia Technology Inc. Device [1b21:2425] (rev 01)
# ./iommu.sh |grep -E "Group 15|Group 31" |grep -v "PCI bridge"
IOMMU Group 15 01:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2b85] (rev a1)
IOMMU Group 15 01:00.1 Audio device [0403]: NVIDIA Corporation Device [10de:22e8] (rev a1)
IOMMU Group 31 1a:00.0 USB controller [0c03]: ASMedia Technology Inc. Device [1b21:2426] (rev 01)
==>
Configuration de GRUB et VFIO:
Voilà, maintenant qu'on a les identifiants des devices pour le passthrough, on configure le système pour les réserver à l'hyperviseur:
GRUB_CMDLINE_LINUX_DEFAULT=" (...) vfio-pci.ids=10de:2b85,10de:22e8,1b21:2426
options vfio-pci ids=10de:2b85,10de:22e8,1b21:2426
blacklist nouveau
blacklist nvidia
blacklist nvidia_drm
blacklist nvidiafb
vfio
vfio_iommu_type1
vfio_pci
vfio_virqfd
update-grub
update-initramfs -c -k $(uname -r)
reboot
Bien vérifier que les autres prérequis sont remplis (déploiement de la machine hôte): OS hôte optimisé pour le matériel, hugepages, configuration du stockage, environnement virtu KVM correct pour passthrough..
Télécharger une image iso de windows 11:
https://www.microsoft.com/fr-fr/software-download/windows11
Télécharger la dernière version de l'iso des virtio guest tools pour windows:
https://github.com/qemus/virtiso-x86/
(Au moment de la rédaction de ce tuto la dernière version est la virtio-win-0.1.271.iso.)
C'est parti:
Nom de la VM: win11.
(car on veut que l'installeur, au démarrage, envoie la vidéo sur la carte virtuelle, pour faire l'installation via la console de virt-manager.)
Créer un volume de stockage pour la nouvelle VM.
Comme j'utilise ZFS je crée un Zvol. C'est à mon avis le meilleur compromis perf/fonctionnalités dans ce cas d'usage. Donc (exemple pour créer un volume de 1.0 TiB pour la nouvelle VM et activer la compression dessus):
# zfs create -V 1024G -o volblocksize=64K -o refreservation=none data/win11-zvol
# zfs set compression=lz4 data/win11-zvol
Pour monitorer la compression en direct:
# zfs get compressratio data/win11-zvol
Pour monitorer l'usage du réseau:
ifstat -i virbr0 1
Créer la VM windows 11:
# virt-install \
--name=win11 \
--os-variant=win11 \
--memory=65536 \
--vcpus=16,sockets=1,cores=8,threads=2 \
--machine q35 \
--features kvm_hidden=on \
--controller type=scsi,model=virtio-scsi \
--cdrom=/system/repository/Win11_24H2_French_x64.iso \
--disk path=/dev/zvol/data/win11-zvol,format=raw,bus=scsi,cache=none,io=native \
--disk path=/system/repository/virtio-win-0.1.271.iso,device=cdrom,bus=sata \
--graphics spice \
--video virtio \
--network network=default,model=virtio \
--wait -1
La topologie CPU en rouge (ici 1x8x2) est à adapter selon les cas particuliers.
Ici dans cet exemple on a un AMD 9950X à 16 cores ; on met 8 cores (16 threads) à la VM donc la moitié du CPU physique (c'est largement suffisant pour l'usage prévu).
Arrêter la VM (pas besoin de laisser l'installeur de Windows se charger).
Activer un iothread: éditer le XML et mettre (sous <vcpu ...>):
<iothreads>1</iothreads>
Les iothreads dans libvirt/QEMU servent à séparer le traitement des i/o du thread principal de la machine virtuelle.
Sans iothreads, toutes les requêtes disque passent par le thread principal. En cas de forte sollicitation ce thread devient un bottleneck.
Avec iothreads, chaque iothread traite les requêtes d'un ou plusieurs périphériques virtio-blk ou virtio-scsi qui lui sont affectés.
Sur une machine possédant beaucoup de coeurs, ça réduit la latence et optimise les perfs globales.
Enfin en règle générale, on doit prévoir 1 iothread par disque virtuel faisant beaucoup d'i/o.
Modifier le contrôleur:
<controller type='scsi' index='0' model='virtio-scsi'>
<driver iothread='1'/>
<address ...>
</controller>
Toujours dans le XML mettre le CPU pinning + iothread:
<vcpu placement='static'>16</vcpu>
<iothreads>1</iothreads>
<cputune>
<vcpupin vcpu='0' cpuset='8'/>
<vcpupin vcpu='1' cpuset='24'/>
<vcpupin vcpu='2' cpuset='9'/>
<vcpupin vcpu='3' cpuset='25'/>
<vcpupin vcpu='4' cpuset='10'/>
<vcpupin vcpu='5' cpuset='26'/>
<vcpupin vcpu='6' cpuset='11'/>
<vcpupin vcpu='7' cpuset='27'/>
<vcpupin vcpu='8' cpuset='12'/>
<vcpupin vcpu='9' cpuset='28'/>
<vcpupin vcpu='10' cpuset='13'/>
<vcpupin vcpu='11' cpuset='29'/>
<vcpupin vcpu='12' cpuset='14'/>
<vcpupin vcpu='13' cpuset='30'/>
<vcpupin vcpu='14' cpuset='15'/>
<vcpupin vcpu='15' cpuset='31'/>
</cputune>
On attribue chaque core logique de la VM à un thread physique du CPU. Il s'agit d'optimiser la performance en faisant fonctionner les bons threads sur les bons cores. Le mapping idéal est différent pour chaque modèle de CPU. Le mieux pour le déterminer est de vérifier avec les commandes lstopo ou numactl --hardware ou encore hwloc.
Ajouter les options suivantes dans la section <hyperv>:
<relaxed state='on'/>
<vapic state='on'/>
<spinlocks state='on' retries='8191'/>
<vpindex state='on'/>
<runtime state='on'/>
<synic state='on'/>
<stimer state='on'/>
<frequencies state='on'/>
<reenlightenment state='on'/>
<tlbflush state='on'/>
<ipi state='on'/>
(Voir la doc de QEMU pour plus de précisions).
Configurer l'utilisation des hugepages (sous la RAM):
<memory unit='KiB'>67108864</memory>
<currentMemory unit='KiB'>67108864</currentMemory>
<memoryBacking>
<hugepages>
<page size='1048576' unit='KiB'/>
</hugepages>
</memoryBacking>
Enregistrer le xml et quitter l'éditeur.
Ajouter les 3 PCI host devices (via la gui virt-manager):
01:00:0 NVIDIA
01:00:1 NVIDIA
1A:00:0 Asmedia
Relancer la VM pour installer windows.
Lors de l'install prendre le driver virtio pour le disque sur E:\vioscsi\w11\amd64 (sur l'image ISO des drivers virtio).
Pour la partie réseau: prendre le driver dans dans E:\NetKVM\win11\amd64.
Pour bypasser le compte microsoft à l'install: quand l'installeur demande le compte microsoft, déconnecter la NIC virtuelle puis faire shift-F10 pour ouvrir une console et taper:
OOBE\BYPASSNRO
Après l'installation du système, installer tous les drivers manquants pour le hardware virtuel depuis l'ISO des drivers virtio.
Installer aussi les Virtio-Win guest tools.
Effectuer maintenant toute la config/personnalisation souhaitée: par exemple, conf IP, etc/hosts, accès NFS au host (en installant Services pour NFS dans les fonctionnalités Windows), activation du RDP +test d'accès, ...etc.
Arrêter la VM win11, l'éditer dans virt-manager pour mettre video=None puis supprimer sound ICH9.
Editer le XML et remplacer <audio id='1' type='spice'/> par <audio id='1' type='none'/>.
Dans mon cas j'ai un CPU unique dans ma machine, un 9950X qui a un seul CCD/un seul noeud NUMA. En cas d'utilisation de matériel de récupération avec des CPU serveurs, et donc en cas de présence de plusieurs nodes NUMA, vérifier ceux-ci sur le host:
# lscpu -e | grep -E 'CPU|[8-9]|1[0-5]|2[4-9]|3[0-1]'
# numactl --hardware
=> Si besoin, pour forcer l'affinité NUMA sur un node, éditer le XML de la VM:
(attention à pinner uniquement les vcpu sur les threads d'un même node numa)
<numatune>
<memory mode='strict' nodeset='0'/>
<memnode cellid='0' mode='strict' nodeset='0'/>
</numatune>
Editer la VM dans virt-manager et vérifier que la topologie CPU est bien 1-8-2 dans <cpu>.
Editer la VM dans virt-manager et supprimer SATA CDROM 2 + démapper l'iso de SATA CDROM 1.
Dans virt-manager supprimer les devices: Channel (spice), USB Redirectors, Display Spice.
Redémarrer la VM et se connecter en RDP ou écran local (il n'y a plus de carte graphique virtuelle donc plus de console virt-manager).
Vérification de KVM et des iothreads depuis le host:
# ps -eLo pid,tid,class,rtprio,ni,pri,psr,pcpu,stat,wchan:14,comm | grep -Ei "kvm|ioth"
=> On doit voir un thread nommé IOThread
Dans la VM Windows, télécharger puis installer le driver pour le GPU.
Ménage dans Paramètres > Applications > Applications installées,
Nettoyage (o&o appbuster ou autre),
Installation d'un browser +ad blocker/extensions
Activer Windows (personnellement je prends des clés à 0,90 EUR sur Rakuten et ça fonctionne très bien).
On a Windows installé sur un disque virtuel mappé à un zvol compressé lz4. Pour que ça ait du sens on ne veut pas chiffrer les données, or Bitlocker est activé par défaut dès l'installation. On va le désactiver (et donc demander à l'OS de déchiffrer puis réécrire toutes les données sur le disque ; ceci est à faire de préférence avant de remplir massivement le disque avec des données):
Ouvrir une console cmd en administrateur:
manage-bde -off C:
Pour surveiller la progression depuis la VM:
manage-bde -status C:
Et pour vérifier côté host:
zfs get compressratio data/vm && df -hP |grep /data
Maintenant que la VM Windows 11 est fraîchement installée et proprement configurée, à ce stade c'est une bonne idée d'en faire une copie:
Arrêter la VM win11.
Faire un backup de l'image initiale:
# zfs snapshot data/win11-zvol@backup
# zfs send data/win11-zvol@backup > /system/vm/backup-diskimg/win11.img
# --ou mieux, en compressant:
# zfs send data/win11-zvol@backup | gzip > /system/vm/backup-diskimg/win11.img.gz
# zfs destroy data/win11-zvol@backup
Pour faire une restauration de l'image initiale:
# zfs create -V 1024G -o volblocksize=64K data/win11-zvol
# gunzip -c /system/repository/images/win11.img.gz | zfs recv -F data/win11-zvol
Pour convertir un zvol en qcow2:
# dd if=/dev/zvol/pool/monzvol of=monzvol.raw bs=1M status=progress
# qemu-img convert -f raw -O qcow2 monzvol.raw monzvol.qcow2
ça va sans dire mais on ne modifie pas le zvol pendant le dd
Opération inverse, pour convertir un qcow2 en zvol:
# zfs create -V 20G pool/monzvol
# qemu-img convert -f qcow2 -O raw monzvol.qcow2 /dev/zvol/pool/monzvol
Contexte: la machine physique est placée loin de chez moi, et utilisée occasionnellement pour le jeu par un utilisateur local.
Objectif: permettre à l'utilisateur local de démarrer/arrêter la VM Windows 11 sans passer par virt-manager. Si jamais une VM est démarrée qui utilise déjà le GPU, ça doit éteindre proprement cette VM automatiquement au passage, puis la relancer une fois la VM Windows 11 éteinte (bien sûr on s'est +/- mis d'accord sur des horaires d'utilisation).
J'utilise un petit pavé numérique USB collé au boitier pour contrôler le démarrage de la VM windows. Ca va sans dire mais ce pavé numérique doit être branché sur un port USB qui n'est pas passé à la VM.
Voici comment j'ai choisi de mettre ça en place. On prévoit les fonctions suivantes (à ajuster selon vos besoins):
La séquence "1 2 3 [entrée]" démarre la VM win11
La séquence "4 5 6 [entrée]" envoie une demande de shutdown à la VM win11 (arrêt propre normal)
La séquence "7 8 9 [entrée]" provoque un arrêt immédiat de la VM win11 (arrêt brutal en cas de plantage)
Pour cela on va créer sur l'hôte les scripts de démarrage/arrêt et créer des comptes utilisateurs sans mots de passes qu'on appellera 123, 456 et 789 et dont le rôle sera de lancer les scripts:
# useradd -m -d /home/123 -s /bin/bash --badname 123 && passwd -d 123
# useradd -m -d /home/456 -s /bin/bash --badname 456 && passwd -d 456
# useradd -m -d /home/789 -s /bin/bash --badname 789 && passwd -d 789
En fonction du paramétrage, le système peut bloquer ces types de comptes (et au fond, il a bien raison). Dans ce cas: ajouter en haut de /etc/pam.d/login:
auth [success=ok default=ignore] pam_succeed_if.so user = 123
auth [success=ok default=ignore] pam_succeed_if.so user = 456
auth [success=ok default=ignore] pam_succeed_if.so user = 789
auth sufficient pam_permit.so
Créer les scripts:
# touch /root/win11_start.sh
# touch /root/win11_shutdown.sh
# touch /root/win11_stop.sh
# chmod +x /root/win11_*.sh
Faire le script de démarrage (/root/win11_start.sh):
#!/bin/sh
# Fichier de log
date >>/root/win11_start.log
exec >>/root/win11_start.log 2>&1
wait_for_state() {
local vm=$1
local expected=$2
local timeout=$3
local elapsed=0
while [ $elapsed -lt $timeout ]; do
state=$(virsh domstate "$vm" | tr -d '\r')
if [[ "$state" == "$expected" || ( -z "$state" && "$expected" == "shut off" ) ]]
then
return 0
fi
sleep 5
elapsed=$((elapsed+5))
done
return 1
}
# Arrêter la VM win11-temp (+suppr. clone/snap) si elle tourne
if [ "$(virsh domstate win11-temp | tr -d '\r')" = "running" ] ;then
echo virsh destroy win11-temp
virsh destroy win11-temp
echo virsh undefine win11-temp --nvram
virsh undefine win11-temp --nvram
echo zfs destroy -R data/win11-zvol-temp
zfs destroy -R data/win11-zvol-temp
echo zfs destroy data/win11-zvol@temp
zfs destroy data/win11-zvol@temp
fi
# arrêter l'autre VM utilisant le GPU si elle est démarrée
if [ "$(virsh domstate ia1 | tr -d '\r')" = "running" ] ;then
echo virsh shutdown ia1
virsh shutdown ia1
if ! wait_for_state ia1 "shut off" 30; then
echo virsh destroy ia1
virsh destroy ia1
fi
fi
# Démarrer win11
if [ "$(virsh domstate win11 | tr -d '\r')" = "shut off" ] ;then
echo virsh start win11
virsh start win11
fi
La présence du bloc concernant une VM "win11-temp" trouve son explication plus loin.
Faire le script de shutdown (/root/win11_shutdown.sh):
#!/bin/sh
# Fichier de log
date >>/root/win11_start.log
exec >>/root/win11_start.log 2>&1
# Arrêter win11 (si elle est démarrée)
if [ "$(virsh domstate win11 | tr -d '\r')" = "running" ] ;then
echo virsh shutdown win11
virsh shutdown win11
fi
Faire le script d'arrêt forcé (/root/win11_stop.sh):
#!/bin/sh
# Fichier de log
date >>/root/win11_start.log
exec >>/root/win11_start.log 2>&1
echo /usr/bin/virsh destroy win11
/usr/bin/virsh destroy win11
echo /usr/bin/virsh destroy win11-temp
/usr/bin/virsh destroy win11-temp
Lancement auto du script start par le user 123:
# vi /home/123/.bashrc
case "$(tty)" in
/dev/tty[0-9]*) sudo /root/win11_start.sh;;
*) echo "logout";;
esac
logout
Lancement auto du script shutdown par le user 456:
# vi /home/456/.bashrc
case "$(tty)" in
/dev/tty[0-9]*) sudo /root/win11_shutdown.sh;;
*) echo "logout";;
esac
logout
Lancement auto du script stop par le user 789:
# vi /home/789/.bashrc
case "$(tty)" in
/dev/tty[0-9]*) sudo /root/win11_stop.sh;;
*) echo "logout";;
esac
logout
Enfin, ajout des commandes dans sudoers pour permettre aux users XXX d'effectuer leurs actions:
# vi /etc/sudoers
123 ALL=(ALL) NOPASSWD: /root/win11_start.sh
456 ALL=(ALL) NOPASSWD: /root/win11_shutdown.sh
789 ALL=(ALL) NOPASSWD: /root/win11_stop.sh
Trois objectifs:
1. Permettre à l'utilisateur local de démarrer sur demande une copie temporaire de la VM Windows, afin de pouvoir faire des tests puis revenir en arrière.
2. Permettre à l'utilisateur local d'effectuer un backup de la VM Windows à la demande, afin de pouvoir faire des tests puis conserver les modifications si tout est OK (et revenir en arrière manuellement sinon).
3. Faire des backups réguliers de la VM Windows en conservant un historique par roulement, de sorte à pouvoir restaurer un état antérieur à tout moment.
On prévoit deux nouvelles séquences de touches sur le pavé numérique pour cette gestion:
La séquence "1 4 7 [entrée]" crée une copie temporaire de win11 et la démarre. Lors de l'arrêt, le cleanup de la copie temporaire est gérée par le script win11_start.sh détaillé plus haut.
La séquence "2 5 8 [entrée]" arrête la VM Windows puis fait un backup et redémarre la VM.
On crée des autres comptes utilisateurs sans mots de passes pour cela:
# useradd -m -d /home/147 -s /bin/bash --badname 147 && passwd -d 147
# useradd -m -d /home/258 -s /bin/bash --badname 258 && passwd -d 258
Script de gestion:
# touch /root/win11_snaptemp.sh && chmod +x /root/win11_snaptemp.sh
# vi /root/win11_snaptemp.sh
#!/bin/bash
#
# win11_snaptemp.sh
#
date >>/root/win11_start.log
exec >>/root/win11_start.log 2>&1
VM="win11"
VM_TEMP="win11-temp"
ZVOL="data/win11-zvol"
SNAP="${ZVOL}@temp"
CLONE="${ZVOL}-temp"
# wait_for_state: attendre qu'une VM ait l'état voulu
wait_for_state() {
local vm=$1
local expected=$2
local timeout=$3
local elapsed=0
while [ $elapsed -lt $timeout ]; do
state=$(virsh domstate "$vm" | tr -d '\r')
if [[ "$state" == "$expected" || ( -z "$state" && "$expected" == "shut off" ) ]]; then
return 0
fi
sleep 5
elapsed=$((elapsed+5))
done
return 1
}
# cleanup: nettoyage si snapshot/clone/temp VM existent
cleanup() {
if zfs list -t snapshot "$SNAP" >/dev/null 2>&1 || zfs list "$CLONE" >/dev/null 2>&1; then
echo "Cleaning leftover snapshot/clone"
if virsh dominfo "$VM_TEMP" >/dev/null 2>&1; then
echo virsh destroy "$VM_TEMP"
virsh destroy "$VM_TEMP" >/dev/null 2>&1 || true
echo irsh undefine "$VM_TEMP" --nvram
virsh undefine "$VM_TEMP" --nvram >/dev/null 2>&1 || true
fi
echo zfs destroy -R "$CLONE"
zfs destroy -R "$CLONE" >/dev/null 2>&1 || true
echo zfs destroy "$SNAP"
zfs destroy "$SNAP" >/dev/null 2>&1 || true
fi
}
## Main
main() {
cleanup
echo cleanup done
# Eteindre la VM si elle tourne
if virsh list --state-running | grep -q "$VM"; then
echo "Shutting down $VM"
echo virsh shutdown "$VM"
virsh shutdown "$VM"
if ! wait_for_state "$VM" "shut off" 30; then
echo virsh shutdown "$VM"
virsh shutdown "$VM"
fi
if ! wait_for_state "$VM" "shut off" 150; then
echo "Timeout waiting for shutdown, forcing off"
echo virsh destroy "$VM"
virsh destroy "$VM"
fi
fi
# Faire le snap et créer la VM temp
echo "Creating temp VM"
echo zfs snapshot "$SNAP"
zfs snapshot "$SNAP"
echo zfs clone "$SNAP" "$CLONE"
zfs clone "$SNAP" "$CLONE"
echo virsh dumpxml "$VM" / virsh define
virsh dumpxml "$VM" \
| sed "s/<name>$VM<\\/name>/<name>$VM_TEMP<\\/name>/" \
| sed "s|<source dev='/dev/zvol/$ZVOL'/>|<source dev='/dev/zvol/$CLONE'/>|" \
| sed "/<uuid>.*<\/uuid>/d" \
| virsh define /dev/stdin
# Injection du wallpaper TEST
echo "Mounting clone to modify wallpaper"
echo kpartx -av /dev/zvol/$CLONE
kpartx -av /dev/zvol/$CLONE
PART_DEV=$(kpartx -av /dev/zvol/$CLONE | awk '{print $3, $6}' | sort -k2,2n | tail -n1 | awk '{print $1}')
mkdir -p /mnt/win11-temp
echo mount -t ntfs-3g /dev/mapper/$PART_DEV /mnt/win11-temp
mount -t ntfs-3g /dev/mapper/$PART_DEV /mnt/win11-temp
echo convert img0.jpg
convert /mnt/win11-temp/Windows/Web/Wallpaper/Windows/img0.jpg \
-gravity center \
-pointsize 550 \
-fill white \
-stroke black -strokewidth 4 \
-annotate -15x-15+0+0 "TEST" \
/mnt/win11-temp/Windows/Web/Wallpaper/Windows/img0.jpg
echo convert img19.jpg
convert /mnt/win11-temp/Windows/Web/Wallpaper/Windows/img19.jpg \
-gravity center \
-pointsize 550 \
-fill white \
-stroke black -strokewidth 4 \
-annotate -15x-15+0+0 "TEST" \
/mnt/win11-temp/Windows/Web/Wallpaper/Windows/img19.jpg
echo convert loop
for user in /mnt/win11-temp/Users/*; do
if [ -f "$user/AppData/Roaming/Microsoft/Windows/Themes/TranscodedWallpaper" ]; then
convert "$user/AppData/Roaming/Microsoft/Windows/Themes/TranscodedWallpaper" \
-gravity center \
-pointsize 550 \
-fill white \
-stroke black -strokewidth 4 \
-annotate -15x-15+0+0 "TEST" \
"$user/AppData/Roaming/Microsoft/Windows/Themes/TranscodedWallpaper"
fi
done
echo umount /mnt/win11-temp
umount /mnt/win11-temp
echo kpartx -dv /dev/zvol/$CLONE
kpartx -dv /dev/zvol/$CLONE
# Démarrer la VM temp
echo virsh start "$VM_TEMP"
virsh start "$VM_TEMP"
}
main
Ce script, avant de démarrer la copie de la VM Windows, monte le disque C: pour aller dedans modifier le fond d'écran du bureau Windows (partie "Injection du wallpaper TEST"), afin de bien signifier à l'utilisateur qu'on est en train de travailler sur une copie temporaire (le wallpaper est modifié pour tous les utilisateurs existants).
Cela nécessite d'avoir préalablement installé imagemagick ou graphicsmagick afin que la commande convert soit disponible pour aller modifier le wallpaper.
Mise en place (comme pour les autres actions):
useradd -m -d /home/147 -s /bin/bash --badname 147 && passwd -d 147
# vi /home/147/.bashrc
case "$(tty)" in
/dev/tty[0-9]*) sudo /root/win11_snaptemp.sh;;
*) echo "logout";;
esac
logout
# vi /etc/sudoers
147 ALL=(ALL) NOPASSWD: /root/win11_snaptemp.sh
Script de gestion:
# touch /root/win11_backup.sh && chmod +x /root/win11_backup.sh
# vi /root/win11_backup.sh
#!/bin/bash
#
# win11_backup.sh
#
date >>/root/win11_start.log
exec >>/root/win11_start.log 2>&1
# wait_for_state: attendre qu'une VM ait l'état voulu
wait_for_state() {
local vm=$1
local expected=$2
local timeout=$3
local elapsed=0
while [ $elapsed -lt $timeout ]; do
state=$(virsh domstate "$vm" | tr -d '\r')
if [[ "$state" == "$expected" || ( -z "$state" && "$expected" == "shut off" ) ]]; then
return 0
fi
sleep 5
elapsed=$((elapsed+5))
done
return 1
}
# Exit if win11 is not running
if [ "$(virsh domstate win11 | tr -d '\r')" = "shut off" ] ;then
echo win11 not running, exiting
exit 0
fi
# shut down win11
echo virsh shutdown win11
virsh shutdown win11
if ! wait_for_state win11 "shut off" 30; then
echo virsh shutdown win11
virsh shutdown win11
fi
if ! wait_for_state win11 "shut off" 150; then
echo cannot shut down win11, cancelling
exit 0
fi
# remove snapshot
echo zfs destroy data/win11-zvol@backup
zfs destroy data/win11-zvol@backup
# create snapshot
echo zfs snapshot data/win11-zvol@backup
zfs snapshot data/win11-zvol@backup
# start win11
echo virsh start win11
virsh start win11
Mise en place:
# useradd -m -d /home/258 -s /bin/bash --badname 258 && passwd -d 258
# vi /home/258/.bashrc
case "$(tty)" in
/dev/tty[0-9]*) sudo /root/win11_backup.sh;;
*) echo "logout";;
esac
logout
# vi /etc/sudoers
258 ALL=(ALL) NOPASSWD: /root/win11_backup.sh
On souhaite faire des snapshots automatiquement tous les jours, en conserver toujours quelques-uns en cas de besoin de rollback, et purger automatiquement les snapshots qui expirent selon une politique de rétention jours/semaines/mois.
Script de gestion:
# touch /root/zfs_autosnap.sh && chmod +x /root/zfs_autosnap.sh
# vi /root/zfs_autosnap.sh
#!/bin/bash
# Vérification du paramètre
if [ -z "$1" ]; then
echo "Usage: $0 <zfs_dataset_or_zvol>"
exit 1
fi
# Argument principal
ZVOL="$1"
# Configuration rétention
KEEPDAY=7 # Nombre de snapshots quotidiens à garder
KEEPWEEK=6 # Nombre de snapshots hebdomadaires à garder
KEEPMONTH=6 # Nombre de snapshots mensuels à garder
PREFIX="autosnap"
# Créer un snapshot horodaté
DATE=$(date +%Y-%m-%d)
SNAP="${ZVOL}@${PREFIX}-${DATE}"
zfs snapshot "$SNAP"
# Lister les snapshots avec ce préfixe
zfs list -H -t snapshot -o name -s creation | grep "^${ZVOL}@${PREFIX}-" > /tmp/snaps.txt
# Trier les snapshots par date extraite (assume format autosnap-YYYY-MM-DD)
mapfile -t SNAPSHOTS < <(cat /tmp/snaps.txt)
# Initialiser des tableaux
declare -A day_snaps
declare -A week_snaps
declare -A month_snaps
for snap in "${SNAPSHOTS[@]}"; do
snap_date=$(echo "$snap" | grep -oP "${PREFIX}-\K[0-9]{4}-[0-9]{2}-[0-9]{2}")
epoch=$(date -d "$snap_date" +%s)
day=$(date -d "$snap_date" +%F)
week=$(date -d "$snap_date" +%G-%V) # année + numéro de semaine
month=$(date -d "$snap_date" +%Y-%m)
day_snaps["$day"]=$snap
week_snaps["$week"]=$snap
month_snaps["$month"]=$snap
done
# Garder les plus récents selon la politique
keep=()
# Derniers KEEPDAY jours
keep+=($(for k in "${!day_snaps[@]}"; do echo "$k"; done | sort -r | head -n $KEEPDAY | while read d; do echo "${day_snaps[$d]}"; done))
# Derniers KEEPWEEK semaines
keep+=($(for k in "${!week_snaps[@]}"; do echo "$k"; done | sort -r | head -n $KEEPWEEK | while read w; do echo "${week_snaps[$w]}"; done))
# Derniers KEEPMONTH mois
keep+=($(for k in "${!month_snaps[@]}"; do echo "$k"; done | sort -r | head -n $KEEPMONTH | while read m; do echo "${month_snaps[$m]}"; done))
# Dédupliquer les snapshots à garder
mapfile -t TO_KEEP < <(printf "%s\n" "${keep[@]}" | sort -u)
# Supprimer les autres snapshots
for snap in "${SNAPSHOTS[@]}"; do
if ! printf "%s\n" "${TO_KEEP[@]}" | grep -qx "$snap"; then
echo "Destroying old snapshot: $snap"
zfs destroy "$snap"
fi
done
Mise en place: ajouter dans la crontab de root:
0 4 * * * /root/zfs_autosnap.sh data/win11-zvol
Dans cet exemple les "sauvegardes" par snapshots seront faites chaque jour à 4h00 du matin, en conservant un historique d'un backup par jour sur 7 jours + un backup par semaine sur 6 semaines + 1 backup par mois sur 6 mois, avant de purger (supprimer) les snapshots expirés selon ce roulement.
Un snapshot n'est pas une sauvegarde ;-)
On veut pouvoir accéder à la VM en SSH. Pas de problème, c'est prévu dans Windows 11.
Dans la VM, ouvrir une console Powershell:
Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH*'
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.
Get-Service sshd
Set-Service sshd -StartupType Automatic
Start-Service sshd
# Restart-Service sshd
New-ItemProperty -Path HKLM:\SOFTWARE\OpenSSH -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force
Authentification par clé:
Générer une clé sur le host:
ssh-keygen -t ed25519
Dans la VM Windows 11, accepter la clé en recopiant le contenu de la partie publique:
powershell Add-Content -Force -Path $env:ProgramData\ssh\administrators_authorized_keys -Value '''ssh-ed25519 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx user@server''';icacls.exe "$env:ProgramData\ssh\administrators_authorized_keys" /inheritance:r /grant "Administrateurs:F" /grant "AUTORITE NT\Système:F"
Exemples de commande utile ensuite: pour voir les utilisateurs actifs (connectés) et les process qu'ils ont lancé:
# Voir si un user a une session active:
$users = query user | Select-Object -Skip 1
foreach ($u in $users) { $u -replace '\s{2,}', ',' | ConvertFrom-Csv -Header User,Session,Id,State,Idle,Logon }
# Conso CPU cumulée des processus en cours d'exécution:
Get-Process | Sort-Object CPU -Descending | Select-Object -First 20 Name, Id, CPU
# Conso CPU des process sur une seconde:
$before = Get-Process | Select-Object Id, ProcessName, CPU
Start-Sleep 1
$after = Get-Process | Select-Object Id, ProcessName, CPU
$after | ForEach-Object {
$old = $before | Where-Object Id -eq $_.Id
if ($old) {
[PSCustomObject]@{
Process = $_.ProcessName
PID = $_.Id
CPUPct = (($_.CPU - $old.CPU) * 100 / $env:NUMBER_OF_PROCESSORS)
}
}
} | Sort-Object CPUPct -Descending | Select-Object -First 10
Si la VM Windows se retrouve à court d'espace disque, no problemo, on peut l'étendre à chaud, c'est un autre avantage de la virtualisation.
Par exemple, on va augmenter de 1.0 TiB à 1.5 TiB.
Sur le host:
# zfs set volsize=1536G data/win11-zvol
Dans la vm win11: lancer un cmd.exe en admin pour désactiver WinRE (Windows Recovery Environment, le truc qui sert à revenir à un point antérieur en cas de système corrompu):
reagentc /disable
Supprimer la partition de récupération:
diskpart
> list volume
> select volume 4
> delete partition override
> exit
Etendre C:\ dans l'outil de gestion des partitions disque de Windows.
Pour réactiver WinRE ensuite si souhaité:
reagentc /enable
1. On s'en fout de WinRE, on peut s'en passer si on a mis en place les snapshots automatiques.
2. Sinon lorsqu'on réactive WinRE, il essayera de créer une partition, sinon il utilisera un fichier dans C:\ si pas de place.
Voici un exemple de méthode à mettre en place pour automatiser les mises à jour Windows Update.
Côté VM, dans une console Powershell:
Stop-Service wuauserv
Set-Service wuauserv -StartupType Disabled
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v NoAutoUpdate /t REG_DWORD /d 1 /f
Install-PackageProvider -Name NuGet -Force
Install-Module -Name PSWindowsUpdate -Force
Editer C:\Scripts\windows_update.ps1:
Import-Module PSWindowsUpdate
Get-WindowsUpdate -AcceptAll -Install -AutoReboot
while ((Get-WURebootStatus).RebootRequired) {
Write-Output "Reboot required, waiting..."
Start-Sleep -Seconds 60
}
Write-Output "All updates installed."
Côté host:
pip install pypsrp
cat >update_vm.sh <<FEOF
#!/bin/bash
VM="win11"
USER="utilisateur"
PASSWORD="mot_de_passe"
HOST="192.168.122.123"
# Démarre la VM
virsh start $VM
echo "VM $VM started."
# Attend que la VM réponde au ping
until ping -c1 $HOST &>/dev/null; do
echo "Waiting for VM network..."
sleep 10
done
echo "VM is up."
# Lance le script PowerShell de mise à jour via WinRM
python3 <<EOF
from pypsrp.client import Client
client = Client("$HOST", username="$USER", password="$PASSWORD", ssl=False)
stdout, stderr, rc = client.execute_cmd("powershell.exe -ExecutionPolicy Bypass -File C:\\Scripts\\windows_update.ps1")
print(stdout)
print(stderr)
EOF
# Boucle d’attente : la VM peut redémarrer plusieurs fois
while virsh domstate $VM | grep -q running; do
echo "VM still running, waiting for final shutdown..."
sleep 60
done
echo "VM $VM is off. Updates complete."
FEOF
Et voilà, il ne reste plus qu'à croner le script update_vm.sh.
Tout marche ! J'ai installé windows seulement 2 fois pour arriver à ce résultat !
Pour une mesure du niveau de performance obtenu, un benchmark Unigine Superposition complet a été lancé et le score était dans le haut du panier, parfaitement équivalent à des scores de configurations bare-metal similaires.
Je n'utilise pas personnellement cette VM Windows pour faire du jeu vidéo mais l'utilisateur qui en bénéficie fait du Flight Simulator 2024 en 4K full details et ça tourne "super bien".
Essayer looking glass (pour avoir une VM avec GPU passthrough mais sans écran connecté dessus physiquement, le flux vidéo passe par le réseau): https://looking-glass.io/
--
TG - 08/2025 update 07/2026