Logo

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

Sommaire

Introduction

Contexte, pré-requis & préparation

Déploiement de la VM

Installation de Windows

Backup initial

Gestion poweron/poweroff VM

Gestion backup et snapshots

Gestion d'une copie temporaire

Gestion du backup à la demande

Gestion des backups automatiques

Login par SSH

Extension du disque virtuel

Automatisation des updates

Conclusion

Introduction

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>

(image générée par IA)

Contexte et pré-requis

Préparation

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)
Très important

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.

Note

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
Autres prérequis

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.)

Procédure

C'est parti:

1. Déploiement de la VM

Nom de la VM: win11.

!! Ne pas brancher d'écran maintenant sur la carte graphique !!

(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>
Note sur les 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'/>
Explication rapide:
  • relaxed: réduit certaines attentes temporelles du kernel guest pour limiter les faux blocages sur la VM.
  • vapic: optimise la gestion de l'APIC virtuel en réduisant le nombre de VM exits lors des interruptions.
  • spinlocks: évite qu'un vCPU boucle indéfiniment sur un spinlock en laissant l'hyperviseur intervenir après un certain nombre de retries.
  • vpindex: fournit à chaque vCPU un identifiant Hyper-V unique, utilisé par d'autres optimisations Hyper-V.
  • runtime: permet à l'OS guest de connaître précisément le temps CPU réellement consommé.
  • synic: active le Synthetic Interrupt Controller, un mécanisme Hyper-V plus efficace pour distribuer les interruptions.
  • stimer: active les Synthetic Timers, des timers virtuels plus performants que les timers matériels émulés.
  • frequencies: expose les fréquences CPU et des timers à l'OS guest pour améliorer les calculs temporels.
  • reenlightenment: Informe Windows des changements de fréquence TSC après une migration ou un changement d'hôte, afin de maintenir une horloge cohérente.
  • tlbflush: Optimise l'invalidation des TLB entre vCPU en évitant des opérations coûteuses.
  • ipi: Optimise l'envoi des Inter-Processor Interrupts (IPI) entre processeurs virtuels.

(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

2. Installation de Windows

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.

Optimisation de la VM

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'/>.

Configuration du NUMA

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

Installation du driver de GPU

Dans la VM Windows, télécharger puis installer le driver pour le GPU.

Optimisation/cleanup OS (facultatif)

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).

Désactivation de Bitlocker

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

3. Backup initial

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
Infos complémentaires/commandes utiles

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

4. Gestion poweron/poweroff VM

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.

Le pavé numérique servant à contrôler le démarrage de la VM.
(image générée par IA)

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
Si ça marche pas:

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
Note

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

5. Ajout de la gestion des snapshots (facultatif)

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
5.1. Gestion d'une copie temporaire

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
Note

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
5.2. Gestion du backup à la demande

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
5.3. Gestion des backups automatiques

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.

Rappel

Un snapshot n'est pas une sauvegarde ;-)

6. Permettre le login par SSH à la VM Windows (facultatif)

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

7. Actions ultérieures facultatives

7.1. Etendre le drive C:\ du zvol Windows (au cas où)

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
Notes

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.

7.2. Automatisation des updates

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.

Résultat

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".

Pour aller plus loin

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

Cet article a été généré par un humain