2025-05-29 19:33:07 +02:00
|
|
|
- name: Setup GPU passthrough host (Fedora + QEMU/KVM)
|
2025-05-31 00:28:04 +02:00
|
|
|
hosts: virtualization
|
2025-05-29 19:33:07 +02:00
|
|
|
become: true
|
|
|
|
|
vars:
|
|
|
|
|
grub_config_file: "/etc/default/grub"
|
|
|
|
|
grub_cmdline_args: >
|
|
|
|
|
rhgb quiet intel_iommu=on iommu=pt
|
2025-05-31 00:28:04 +02:00
|
|
|
isolcpus={{ virt_vm_reserved_cpu }} nohz_full={{ virt_vm_reserved_cpu }} rcu_nocbs={{ virt_vm_reserved_cpu }}
|
2025-05-29 19:33:07 +02:00
|
|
|
snd_hda_codec_hdmi.blacklist=1 rd.driver.pre=vfio-pci audit=off
|
2025-05-31 00:28:04 +02:00
|
|
|
default_hugepagesz=1G hugepagesz=1G hugepages=8 mitigations=off loglevel=3 {{ virt_extra_grub_args | default("") | trim}}
|
2025-05-29 19:33:07 +02:00
|
|
|
|
|
|
|
|
tasks:
|
2026-01-02 23:15:08 +01:00
|
|
|
- name: Ensure the system is Fedora
|
|
|
|
|
ansible.builtin.assert:
|
|
|
|
|
that:
|
|
|
|
|
- ansible_facts['distribution'] == "Fedora"
|
|
|
|
|
fail_msg: "This playbook only supports Fedora hosts."
|
|
|
|
|
|
|
|
|
|
- name: Install python3-libdnf5 so Ansible DNF module works
|
|
|
|
|
become: true
|
|
|
|
|
ansible.builtin.shell:
|
|
|
|
|
cmd: |
|
|
|
|
|
dnf install -y python3-libdnf5
|
|
|
|
|
touch /etc/.python3_libdnf5_installed
|
|
|
|
|
args:
|
|
|
|
|
creates: /etc/.python3_libdnf5_installed
|
|
|
|
|
|
2025-05-31 00:28:04 +02:00
|
|
|
- name: Install required packages
|
2025-05-29 19:33:07 +02:00
|
|
|
ansible.builtin.dnf:
|
|
|
|
|
name:
|
|
|
|
|
- qemu-kvm
|
|
|
|
|
- libvirt
|
|
|
|
|
- virt-install
|
|
|
|
|
- virt-manager
|
|
|
|
|
- bridge-utils
|
|
|
|
|
- pciutils
|
2025-05-31 00:28:04 +02:00
|
|
|
- tuned
|
|
|
|
|
- python3-lxml
|
|
|
|
|
- python3-libvirt
|
2026-01-02 23:15:08 +01:00
|
|
|
- tuned
|
|
|
|
|
- cloud-utils
|
|
|
|
|
- qemu-img
|
2025-05-29 19:33:07 +02:00
|
|
|
state: present
|
|
|
|
|
|
2026-01-02 23:15:08 +01:00
|
|
|
# ===== BRIDGE CONFIGURATION TASKS =====
|
|
|
|
|
|
|
|
|
|
- name: Detect current network interface
|
|
|
|
|
ansible.builtin.set_fact:
|
|
|
|
|
primary_interface: "{{ bridge_interface }}"
|
|
|
|
|
when: bridge_interface is defined
|
|
|
|
|
|
|
|
|
|
- name: Check if bridge already exists
|
|
|
|
|
ansible.builtin.command: nmcli -t -f NAME,TYPE connection show
|
|
|
|
|
register: nmcli_connections
|
|
|
|
|
changed_when: false
|
|
|
|
|
failed_when: false
|
|
|
|
|
|
|
|
|
|
- name: Set bridge existence fact
|
|
|
|
|
ansible.builtin.set_fact:
|
|
|
|
|
bridge_exists: "{{ (bridge_name in nmcli_connections.stdout) and ('bridge' in nmcli_connections.stdout) }}"
|
|
|
|
|
|
|
|
|
|
- name: Get current interface connection name
|
|
|
|
|
ansible.builtin.shell:
|
|
|
|
|
cmd: |
|
|
|
|
|
set -o pipefail
|
|
|
|
|
nmcli -t -f NAME,DEVICE connection show --active | grep ":{{ primary_interface }}$" | cut -d: -f1 | head -n1
|
|
|
|
|
executable: /bin/bash
|
|
|
|
|
register: current_connection_name
|
|
|
|
|
changed_when: false
|
|
|
|
|
when: not bridge_exists
|
|
|
|
|
|
|
|
|
|
- name: Create bridge connection with static IPv4
|
|
|
|
|
ansible.builtin.command: >
|
|
|
|
|
nmcli connection add
|
|
|
|
|
type bridge
|
|
|
|
|
con-name {{ bridge_name }}
|
|
|
|
|
ifname {{ bridge_name }}
|
|
|
|
|
ipv4.method manual
|
|
|
|
|
ipv4.addresses {{ bridge_ipv4_address }}
|
|
|
|
|
ipv4.gateway {{ bridge_ipv4_gateway }}
|
|
|
|
|
ipv4.dns "{{ bridge_ipv4_dns }}"
|
|
|
|
|
ipv4.dns-search {{ bridge_ipv4_domain }}
|
|
|
|
|
ipv6.method disabled
|
|
|
|
|
connection.autoconnect yes
|
|
|
|
|
bridge.stp no
|
|
|
|
|
when: not bridge_exists
|
|
|
|
|
changed_when: true
|
|
|
|
|
register: bridge_created
|
|
|
|
|
|
|
|
|
|
- name: Create bridge slave connection for physical interface
|
|
|
|
|
ansible.builtin.command: >
|
|
|
|
|
nmcli connection add
|
|
|
|
|
type ethernet
|
|
|
|
|
con-name {{ bridge_name }}-slave
|
|
|
|
|
ifname {{ primary_interface }}
|
|
|
|
|
master {{ bridge_name }}
|
|
|
|
|
slave-type bridge
|
|
|
|
|
when: not bridge_exists and bridge_created is succeeded
|
|
|
|
|
changed_when: true
|
|
|
|
|
register: slave_created
|
|
|
|
|
|
|
|
|
|
- name: Deactivate old interface connection and activate bridge
|
|
|
|
|
become: true
|
|
|
|
|
ansible.builtin.command: >
|
|
|
|
|
nmcli connection down "{{ current_connection_name.stdout }}"
|
|
|
|
|
nmcli connection up {{ bridge_name }}
|
|
|
|
|
when:
|
|
|
|
|
- not bridge_exists
|
|
|
|
|
- slave_created is succeeded
|
|
|
|
|
- current_connection_name.stdout | length > 0
|
|
|
|
|
register: bridge_activated
|
|
|
|
|
changed_when: true
|
|
|
|
|
failed_when: false
|
|
|
|
|
async: 10
|
|
|
|
|
poll: 0
|
|
|
|
|
|
|
|
|
|
- name: Wait for network connectivity to restore # noqa: no-handler
|
|
|
|
|
ansible.builtin.wait_for:
|
|
|
|
|
host: "{{ bridge_ipv4_address.split('/')[0] }}"
|
|
|
|
|
port: 22
|
|
|
|
|
timeout: 30
|
|
|
|
|
delegate_to: localhost
|
|
|
|
|
when: bridge_activated is changed
|
|
|
|
|
|
|
|
|
|
- name: Switch ansible to connect via bridge IP # noqa: no-handler
|
|
|
|
|
ansible.builtin.set_fact:
|
|
|
|
|
ansible_host: "{{ bridge_ipv4_address.split('/')[0] }}"
|
|
|
|
|
when: bridge_activated is changed
|
|
|
|
|
|
|
|
|
|
- name: Verify bridge is active # noqa: no-handler
|
|
|
|
|
ansible.builtin.command: ip addr show {{ bridge_name }}
|
|
|
|
|
register: bridge_status
|
|
|
|
|
changed_when: false
|
|
|
|
|
failed_when: "'state UP' not in bridge_status.stdout"
|
|
|
|
|
when: bridge_activated is changed
|
|
|
|
|
|
|
|
|
|
- name: Display bridge configuration
|
|
|
|
|
ansible.builtin.debug: # noqa: no-handler
|
|
|
|
|
msg:
|
|
|
|
|
- "Bridge {{ bridge_name }} is configured and active"
|
|
|
|
|
- "IP Address: {{ bridge_ipv4_address }}"
|
|
|
|
|
- "Gateway: {{ bridge_ipv4_gateway }}"
|
|
|
|
|
- "DNS: {{ bridge_ipv4_dns }}"
|
|
|
|
|
- "Physical interface: {{ primary_interface }}"
|
|
|
|
|
when: bridge_activated is changed
|
|
|
|
|
|
|
|
|
|
# ===== END BRIDGE CONFIGURATION =====
|
|
|
|
|
|
2025-05-29 19:33:07 +02:00
|
|
|
- name: Enable and start libvirtd
|
|
|
|
|
ansible.builtin.systemd:
|
|
|
|
|
name: libvirtd
|
|
|
|
|
enabled: true
|
|
|
|
|
state: started
|
|
|
|
|
|
|
|
|
|
- name: Ensure GRUB_CMDLINE_LINUX is configured for vfio, isolcpus, nohz_full, rcu_nocbs
|
|
|
|
|
ansible.builtin.lineinfile:
|
|
|
|
|
path: "{{ grub_config_file }}"
|
|
|
|
|
regexp: '^GRUB_CMDLINE_LINUX='
|
|
|
|
|
line: 'GRUB_CMDLINE_LINUX="{{ grub_cmdline_args | trim }}"'
|
|
|
|
|
backrefs: true
|
2025-05-31 00:28:04 +02:00
|
|
|
notify: Rebuild GRUB config
|
2025-05-29 19:33:07 +02:00
|
|
|
|
2026-01-02 23:15:08 +01:00
|
|
|
- name: Copy vfio-bind script
|
|
|
|
|
ansible.builtin.copy:
|
|
|
|
|
src: vfio-bind.sh
|
|
|
|
|
dest: /usr/local/bin/vfio-bind.sh
|
|
|
|
|
mode: '0755'
|
|
|
|
|
|
|
|
|
|
- name: Create systemd service for vfio-bind
|
|
|
|
|
ansible.builtin.copy:
|
|
|
|
|
dest: "/etc/systemd/system/vfio-{{ item.vendor }}-{{ item.device }}-bind.service"
|
|
|
|
|
content: |
|
|
|
|
|
[Unit]
|
|
|
|
|
Description=Bind PCI device {{ item.vendor }}:{{ item.device }} to VFIO driver
|
|
|
|
|
After=systemd-udev-settle.service
|
|
|
|
|
Wants=systemd-udev-settle.service
|
|
|
|
|
Before=libvirtd.service
|
|
|
|
|
|
|
|
|
|
[Service]
|
|
|
|
|
Type=oneshot
|
|
|
|
|
ExecStart=/usr/local/bin/vfio-bind.sh "{{ item.dev }}" "{{ item.vendor }}" "{{ item.device }}"
|
|
|
|
|
RemainAfterExit=yes
|
|
|
|
|
|
|
|
|
|
[Install]
|
|
|
|
|
WantedBy=multi-user.target
|
|
|
|
|
mode: '0644'
|
|
|
|
|
loop: "{{ virt_vfio_devices }}"
|
|
|
|
|
|
|
|
|
|
- name: Enable and start VFIO bind services
|
|
|
|
|
ansible.builtin.systemd:
|
|
|
|
|
name: "vfio-{{ item.vendor }}-{{ item.device }}-bind.service"
|
|
|
|
|
enabled: true
|
|
|
|
|
state: started
|
|
|
|
|
daemon_reload: true
|
|
|
|
|
loop: "{{ virt_vfio_devices }}"
|
|
|
|
|
|
2025-05-29 19:33:07 +02:00
|
|
|
- name: Add VFIO PCI device IDs to modprobe
|
|
|
|
|
ansible.builtin.copy:
|
|
|
|
|
dest: /etc/modprobe.d/vfio.conf
|
|
|
|
|
content: |
|
2025-05-31 00:28:04 +02:00
|
|
|
options vfio-pci ids={{ virt_vfio_ids | join(',') }}
|
2025-05-29 19:33:07 +02:00
|
|
|
install snd_hda_codec_hdmi /bin/false
|
2026-01-02 23:15:08 +01:00
|
|
|
softdep xhci_hcd pre: vfio-pci
|
2025-05-29 19:33:07 +02:00
|
|
|
mode: '0644'
|
|
|
|
|
|
|
|
|
|
- name: Blacklist Nouveau and other drivers
|
2025-05-31 00:28:04 +02:00
|
|
|
ansible.builtin.template:
|
|
|
|
|
src: blacklist.conf.j2
|
2025-05-29 19:33:07 +02:00
|
|
|
dest: /etc/modprobe.d/ansible-blacklist.conf
|
|
|
|
|
mode: '0644'
|
2025-05-31 00:28:04 +02:00
|
|
|
when: virt_blacklist is defined
|
2025-05-29 19:33:07 +02:00
|
|
|
|
|
|
|
|
- name: Add vfio modules to initramfs config
|
|
|
|
|
ansible.builtin.copy:
|
|
|
|
|
dest: /etc/dracut.conf.d/vfio.conf
|
|
|
|
|
content: |
|
|
|
|
|
add_drivers+=" vfio vfio_iommu_type1 vfio_pci "
|
|
|
|
|
mode: '0644'
|
2025-05-31 00:28:04 +02:00
|
|
|
notify: Rebuild initramfs
|
2025-05-29 19:33:07 +02:00
|
|
|
|
2025-05-31 00:28:04 +02:00
|
|
|
- name: Configure hugepages
|
|
|
|
|
ansible.posix.sysctl:
|
|
|
|
|
name: vm.nr_hugepages
|
|
|
|
|
value: "{{ hugepages_count | default('2048') }}"
|
|
|
|
|
state: present
|
|
|
|
|
reload: true
|
2025-05-29 19:33:07 +02:00
|
|
|
|
|
|
|
|
- name: Enable KVM modules at boot
|
|
|
|
|
ansible.builtin.copy:
|
|
|
|
|
dest: /etc/modules-load.d/kvm.conf
|
|
|
|
|
content: |
|
|
|
|
|
kvmgt
|
|
|
|
|
mdev
|
|
|
|
|
vfio
|
|
|
|
|
vfio-pci
|
|
|
|
|
vfio_iommu_type1
|
|
|
|
|
mode: '0644'
|
|
|
|
|
|
2025-05-31 00:28:04 +02:00
|
|
|
- name: Set I/O scheduler to 'none' for NVMes
|
|
|
|
|
ansible.builtin.copy:
|
|
|
|
|
dest: /etc/udev/rules.d/60-ioscheduler.rules
|
|
|
|
|
content: |
|
|
|
|
|
ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/scheduler}="none"
|
|
|
|
|
mode: '0644'
|
|
|
|
|
notify:
|
|
|
|
|
- Reload udev rules
|
|
|
|
|
- Trigger udev to apply changes
|
2025-05-29 19:33:07 +02:00
|
|
|
|
2025-05-31 00:28:04 +02:00
|
|
|
- name: Activate virtual-host tuned profile
|
|
|
|
|
ansible.builtin.command: tuned-adm profile virtual-host
|
|
|
|
|
changed_when: false
|
2025-05-29 19:33:07 +02:00
|
|
|
|
|
|
|
|
- name: Set permissions for partition passthrough
|
|
|
|
|
ansible.builtin.file:
|
2025-05-31 00:28:04 +02:00
|
|
|
path: "/dev/{{ item }}"
|
2025-05-29 19:33:07 +02:00
|
|
|
owner: qemu
|
|
|
|
|
group: kvm
|
|
|
|
|
mode: '0660'
|
2025-05-31 00:28:04 +02:00
|
|
|
loop: "{{ virt_raw_block_devices }}"
|
2025-05-29 19:33:07 +02:00
|
|
|
|
|
|
|
|
- name: Create udev rules for persistent access
|
2025-05-31 00:28:04 +02:00
|
|
|
ansible.builtin.template:
|
|
|
|
|
src: 99-qemu-nvme.rules.j2
|
2025-05-29 19:33:07 +02:00
|
|
|
dest: /etc/udev/rules.d/99-qemu-nvme.rules
|
|
|
|
|
mode: '0644'
|
|
|
|
|
notify:
|
|
|
|
|
- Reload udev rules
|
|
|
|
|
- Trigger udev to apply changes
|
|
|
|
|
|
2026-01-02 23:15:08 +01:00
|
|
|
- name: Download and resize cloud image for VM
|
|
|
|
|
block:
|
|
|
|
|
- name: Check if local image already exists
|
|
|
|
|
vars:
|
|
|
|
|
vm: "{{ item }}"
|
|
|
|
|
ansible.builtin.stat:
|
|
|
|
|
path: "/var/lib/libvirt/images/{{ vm.disk_file }}"
|
|
|
|
|
get_checksum: false
|
|
|
|
|
register: cloud_image_stat
|
|
|
|
|
loop: "{{ virt_machines }}"
|
|
|
|
|
|
|
|
|
|
- name: Download cloud image if not present
|
|
|
|
|
vars:
|
|
|
|
|
id: "{{ item }}"
|
|
|
|
|
loop: "{{ range(virt_machines | length) }}"
|
|
|
|
|
ansible.builtin.get_url:
|
|
|
|
|
url: "{{ virt_machines[id].cloud_image_url }}"
|
|
|
|
|
dest: "/var/lib/libvirt/images/{{ virt_machines[id].disk_file }}"
|
|
|
|
|
mode: '0644'
|
|
|
|
|
checksum: "sha256:{{ virt_machines[id].cloud_image_sha256 }}"
|
|
|
|
|
when:
|
|
|
|
|
- not cloud_image_stat.results[id].stat.exists
|
|
|
|
|
- virt_machines[id].cloud_image_url is defined
|
|
|
|
|
- virt_machines[id].cloud_image_sha256 is defined
|
|
|
|
|
|
|
|
|
|
- name: Resize cloud image
|
|
|
|
|
vars:
|
|
|
|
|
id: "{{ item }}"
|
|
|
|
|
loop: "{{ range(virt_machines | length) }}"
|
|
|
|
|
ansible.builtin.command: >
|
|
|
|
|
qemu-img resize /var/lib/libvirt/images/{{ virt_machines[id].disk_file }} {{ virt_machines[id].disk_size | default('20G') }}
|
|
|
|
|
when:
|
|
|
|
|
- not cloud_image_stat.results[id].stat.exists
|
|
|
|
|
- virt_machines[id].disk_size is defined
|
|
|
|
|
changed_when: false
|
|
|
|
|
|
|
|
|
|
- name: Create cloud-init ISO for VM
|
|
|
|
|
block:
|
|
|
|
|
- name: Create temporary cloud-init directory
|
|
|
|
|
vars:
|
|
|
|
|
vm: "{{ item }}"
|
|
|
|
|
loop: "{{ virt_machines }}"
|
|
|
|
|
ansible.builtin.file:
|
|
|
|
|
path: "/var/lib/libvirt/images/{{ vm.name }}-cloudinit"
|
|
|
|
|
state: directory
|
|
|
|
|
mode: '0755'
|
|
|
|
|
when: vm.user_data is defined
|
|
|
|
|
|
|
|
|
|
- name: Create user-data file
|
|
|
|
|
vars:
|
|
|
|
|
vm: "{{ item }}"
|
|
|
|
|
loop: "{{ virt_machines }}"
|
|
|
|
|
ansible.builtin.copy:
|
|
|
|
|
dest: "/var/lib/libvirt/images/{{ vm.name }}-cloudinit/user-data"
|
|
|
|
|
content: |
|
|
|
|
|
{{ vm.user_data }}
|
|
|
|
|
mode: '0644'
|
|
|
|
|
when: vm.user_data is defined
|
|
|
|
|
register: user_data_created
|
|
|
|
|
|
|
|
|
|
- name: Create meta-data file
|
|
|
|
|
vars:
|
|
|
|
|
vm: "{{ item }}"
|
|
|
|
|
loop: "{{ virt_machines }}"
|
|
|
|
|
ansible.builtin.copy:
|
|
|
|
|
dest: "/var/lib/libvirt/images/{{ vm.name }}-cloudinit/meta-data"
|
|
|
|
|
content: |
|
|
|
|
|
instance-id: {{ vm.name }}-{{ vm.uuid }}
|
|
|
|
|
local-hostname: {{ vm.name }}
|
|
|
|
|
mode: '0644'
|
|
|
|
|
when: vm.user_data is defined
|
|
|
|
|
|
|
|
|
|
- name: Create cloud-init ISO image
|
|
|
|
|
vars:
|
|
|
|
|
vm: "{{ item }}"
|
|
|
|
|
loop: "{{ virt_machines }}"
|
|
|
|
|
ansible.builtin.command: >
|
|
|
|
|
cloud-localds
|
|
|
|
|
/var/lib/libvirt/images/{{ vm.name }}-cloudinit.img
|
|
|
|
|
/var/lib/libvirt/images/{{ vm.name }}-cloudinit/user-data
|
|
|
|
|
/var/lib/libvirt/images/{{ vm.name }}-cloudinit/meta-data
|
|
|
|
|
args:
|
|
|
|
|
creates: "/var/lib/libvirt/images/{{ vm.name }}-cloudinit.img"
|
|
|
|
|
when: vm.user_data is defined
|
|
|
|
|
|
2025-05-31 00:28:04 +02:00
|
|
|
- name: Create or update virtual machine
|
|
|
|
|
vars:
|
|
|
|
|
vm: "{{ item }}"
|
|
|
|
|
loop: "{{ virt_machines }}"
|
|
|
|
|
community.libvirt.virt:
|
|
|
|
|
autostart: "{{ vm.autostart | default(false) }}"
|
|
|
|
|
name: "{{ vm.name }}"
|
|
|
|
|
command: define
|
|
|
|
|
xml: "{{ lookup('template', 'domain.xml.j2') }}"
|
|
|
|
|
|
|
|
|
|
- name: Ensure virtual machines are running
|
|
|
|
|
vars:
|
|
|
|
|
vm: "{{ item }}"
|
|
|
|
|
loop: "{{ virt_machines }}"
|
|
|
|
|
community.libvirt.virt:
|
|
|
|
|
name: "{{ vm.name }}"
|
|
|
|
|
state: running
|
2026-01-02 23:15:08 +01:00
|
|
|
when: vm.autostart | default(false)
|
2025-05-31 00:28:04 +02:00
|
|
|
|
|
|
|
|
- name: Notify user to reboot for changes
|
|
|
|
|
ansible.builtin.debug:
|
|
|
|
|
msg: |
|
|
|
|
|
Reboot your system to apply IOMMU, CPU isolation, and VFIO settings.
|
|
|
|
|
After reboot, verify using: `dmesg | grep -i vfio` and `lspci -nnk`
|
|
|
|
|
|
2025-05-29 19:33:07 +02:00
|
|
|
handlers:
|
|
|
|
|
- name: Reload udev rules
|
|
|
|
|
changed_when: false
|
|
|
|
|
ansible.builtin.command: udevadm control --reload-rules
|
|
|
|
|
|
|
|
|
|
- name: Trigger udev to apply changes
|
|
|
|
|
changed_when: false
|
|
|
|
|
ansible.builtin.command: udevadm trigger
|
2025-05-31 00:28:04 +02:00
|
|
|
|
|
|
|
|
- name: Rebuild initramfs
|
|
|
|
|
changed_when: false
|
|
|
|
|
ansible.builtin.command: dracut -f
|
|
|
|
|
|
|
|
|
|
- name: Rebuild GRUB config
|
|
|
|
|
changed_when: false
|
|
|
|
|
ansible.builtin.command: grub2-mkconfig -o /boot/grub2/grub.cfg
|
|
|
|
|
when: ansible_facts['distribution'] == "Fedora"
|