diff --git a/README.md b/README.md index 9ed2c3c..22822c0 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ Role Variables - `autostart`: Whether to start the VM when the host starts up. Default is `true`. + + - `boot_firmware`: Can be one of: `bios`, or `efi`. Defaults to `bios`. N.B. the following variables are deprecated: `libvirt_vm_state`, diff --git a/defaults/main.yml b/defaults/main.yml index 027618d..4e8fe7f 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -24,6 +24,16 @@ libvirt_vm_engine: # correct emulator to use. libvirt_vm_emulator: +# This is where https://github.com/puiterwijk/qemu-ovmf-secureboot will be +# checked out to. +libvirt_ovmf_vars_generator_checkout_path: /opt/qemu-ovmf-secureboot + +# Where to output the generated variable store +libvirt_ovmf_vars_generator_output_path: /var/lib/libvirt/qemu/ + +# Prefix of generated variable file name. The checksum of the input will be appended. +libvirt_ovmf_vars_generator_output_prefix: ovmf_vars_enrolled_ + # A list of specifications of VMs to be created. # For backwards compatibility, libvirt_vms defaults to a singleton list using # the values of the deprecated variables below. @@ -56,6 +66,9 @@ libvirt_vms: # Path to console log file. console_log_path: "{{ libvirt_vm_console_log_path }}" + # May be one of: bios, or efi. + boot_firmware: bios + ### DEPRECATED ### # Use the above settings for each item within `libvirt_vms`, instead of the diff --git a/tasks/autodetect.yml b/tasks/autodetect.yml index a29647c..3ac188e 100644 --- a/tasks/autodetect.yml +++ b/tasks/autodetect.yml @@ -14,26 +14,27 @@ - name: Set a fact containing the virtualisation engine set_fact: - libvirt_vm_engine: "{% if stat_kvm.stat.exists %}kvm{% else %}qemu{% endif %}" + libvirt_vm_engine: >- + {%- if ansible_architecture != libvirt_vm_arch -%} + {# Virtualisation instructions are generally available only for the host + architecture. Ideally we would test for virtualisation instructions, eg. vt-d + as it is possible that another architecture could support these even + if the emulated cpu architecture is not the same. #} + qemu + {%- elif stat_kvm.stat.exists -%} + kvm + {%- else -%} + qemu + {%- endif -%} when: libvirt_vm_engine is none - name: Detect the virtualisation emulator block: - - block: - - name: Detect the KVM emulator binary path - stat: - path: "{{ item }}" - register: kvm_emulator_result - with_items: - - /usr/bin/kvm - - /usr/bin/qemu-kvm - - /usr/libexec/qemu-kvm - - - name: Set a fact containing the KVM emulator binary path - set_fact: - libvirt_vm_emulator: "{{ item.item }}" - with_items: "{{ kvm_emulator_result.results }}" - when: item.stat.exists + - name: Ensure old value stored in detected_libvirt_vm_emulator is cleared + set_fact: + detected_libvirt_vm_emulator: none + + - include_tasks: detect-kvm.yml when: libvirt_vm_engine == 'kvm' - block: @@ -44,11 +45,22 @@ - name: Set a fact containing the QEMU emulator binary path set_fact: - libvirt_vm_emulator: "{{ qemu_emulator_result.stdout }}" + detected_libvirt_vm_emulator: "{{ qemu_emulator_result.stdout }}" + + - name: Override the QEMU emulator binary path on RedHat based distros + # CentOS 7.5 ships qemu-system-x86-2.0.0-1.el7.6.x86_64, this + # does not have smm support. We can use the qemu-kvm binary + # from SIG virtualization repository. + include_tasks: detect-kvm.yml + when: + - enable_feature_smm + - ansible_os_family == "RedHat" + - ansible_architecture == libvirt_vm_arch + when: libvirt_vm_engine == 'qemu' - name: Fail if unable to detect the emulator fail: msg: Unable to detect emulator for engine {{ libvirt_vm_engine }}. - when: libvirt_vm_emulator is none + when: detected_libvirt_vm_emulator is none when: libvirt_vm_emulator is none diff --git a/tasks/destroy-vm.yml b/tasks/destroy-vm.yml index 4cc3313..a024645 100644 --- a/tasks/destroy-vm.yml +++ b/tasks/destroy-vm.yml @@ -16,8 +16,11 @@ become: yes - name: Ensure the VM is undefined - virt: - name: "{{ vm.name }}" - command: undefine + # note(wszumski): the virt module does not seem to support + # removing vms with nvram defined - as a workaround, use the + # virsh cli directly. It may be better to detect if dumpxml + # actually contains an nvram element rather than relying on + # boot_firmware having the correct value. + command: virsh -c qemu:///system undefine{% if boot_firmware == 'efi' %} --nvram{% endif %} {{ vm.name }} become: yes when: vm.name in result.list_vms diff --git a/tasks/detect-kvm.yml b/tasks/detect-kvm.yml new file mode 100644 index 0000000..e7a5120 --- /dev/null +++ b/tasks/detect-kvm.yml @@ -0,0 +1,16 @@ +--- + +- name: Detect the KVM emulator binary path + stat: + path: "{{ item }}" + register: kvm_emulator_result + with_items: + - /usr/bin/kvm + - /usr/bin/qemu-kvm + - /usr/libexec/qemu-kvm + +- name: Set a fact containing the KVM emulator binary path + set_fact: + detected_libvirt_vm_emulator: "{{ item.item }}" + with_items: "{{ kvm_emulator_result.results }}" + when: item.stat.exists diff --git a/tasks/main.yml b/tasks/main.yml index 08e2cdf..416a88b 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,10 +1,4 @@ --- -- include_tasks: autodetect.yml - # We don't need to know the engine and emulator if we're not creating any new - # VMs. - when: >- - (libvirt_vms | selectattr('state', 'defined') - | selectattr('state', 'equalto', 'absent') | list) != libvirt_vms - include_tasks: volumes.yml vars: @@ -18,7 +12,14 @@ vars: console_log_enabled: "{{ vm.console_log_enabled | default(false) }}" console_log_path: "{{ vm.console_log_path | default(libvirt_vm_default_console_log_dir + '/' + vm.name + '-console.log', true) }}" - machine_default: "{{ none if libvirt_vm_engine == 'kvm' else 'pc-1.0' }}" + machine_default: >- + {%- if boot_firmware == 'efi' -%} + q35 + {%- elif libvirt_vm_engine == 'kvm' -%} + none + {%- else -%} + pc + {%- endif -%} machine: "{{ vm.machine | default(machine_default, true) }}" cpu_mode_default: "{{ 'host-passthrough' if libvirt_vm_engine == 'kvm' else 'host-model' }}" cpu_mode: "{{ vm.cpu_mode | default(cpu_mode_default, true) }}" @@ -26,6 +27,12 @@ interfaces: "{{ vm.interfaces | default([], true) }}" start: "{{ vm.start | default(true) }}" autostart: "{{ vm.autostart | default(true) }}" + boot_firmware: "{{ vm.boot_firmware | default('bios', true) | lower }}" + enable_feature_acpi_default: "{{ true if boot_firmware == 'efi' else false }}" + enable_feature_acpi: "{{ vm.enable_feature_acpi | default(enable_feature_acpi_default, true) }}" + enable_feature_smm_default: "{{ enable_secure_boot }}" + enable_feature_smm: "{{ vm.enable_feature_smm | default(enable_feature_smm_default, true) }}" + enable_secure_boot: "{{ vm.enable_secure_boot | default(false, true) | bool }}" with_items: "{{ libvirt_vms }}" loop_control: loop_var: vm @@ -40,6 +47,8 @@ when: (vm.state | default('present', true)) == 'absent' - include_tasks: destroy-vm.yml + vars: + boot_firmware: "{{ vm.boot_firmware | default('bios', true) | lower }}" with_items: "{{ libvirt_vms }}" loop_control: loop_var: vm diff --git a/tasks/prepare-secure-boot.yml b/tasks/prepare-secure-boot.yml new file mode 100644 index 0000000..d03d9af --- /dev/null +++ b/tasks/prepare-secure-boot.yml @@ -0,0 +1,107 @@ +--- +# This playbook enrolls Platform, Key Exchange, and Signature Database keys +# in the emulated NVRAM. NVRAM is the storage location for persistent +# EFI state. The keys that are installed are as follows: +# +# * Platform Key: Red Hat Secure Boot (PK/KEK key 1)/emailAddress=secalert@redhat.com. +# This is a Red Hat controlled key which controls modification to the +# Key Exchange Keys (KEK). +# +# * Key Exchange Keys: +# 1) Microsoft Corporation KEK CA 2011 +# 2) Red Hat Secure Boot (PK/KEK key 1)/emailAddress=secalert@redhat.com +# +# The first KEK is used to sign the revocation database obtained from: +# http://www.uefi.org/revocationlistfile. This allows you to use: dbxtool +# to periodically update your local copy of this blacklist. The second gives +# RedHat control over the dbx (Forbidden Signature) and db (Signature) databases. +# This essentially makes your computer trust RedHat and Microsoft signed binaries. +# +# * Signature database (db) keys: +# - Microsoft Windows Production PCA 2011 (for accepting Windows 8, Windows Server 2012 R2, etc boot loaders) +# - Microsoft Corporation UEFI CA 2011 (for verifying the shim binary, and PCI expansion ROMs). +# +# Further signing keys can be enrolled in the shim binary to allow execution of custom binaries. +# +# When a platform key is enrolled, the secure boot mode changes from "setup mode" to "user mode" +# and secure boot is automatically enabled. Secure boot can only be disabled via the EFI setup +# menu - this is accessed by pressing delete when the VM is started. It does not seem possible to +# to control this setting via libvirt or by using a qemu command line option. +# +# TODO: Allow installation of custom keys + +- name: Gather os specific variables + include_vars: "{{ item }}" + with_first_found: + - files: + - "{{ ansible_distribution }}-{{ ansible_distribution_major_version}}.yml" + - "{{ ansible_distribution }}.yml" + - "{{ ansible_os_family }}.yml" + skip: true + tags: vars + +- name: Ensure ovmf generator checkout directory is owned by ansible_user + file: + path: "{{ libvirt_ovmf_vars_generator_checkout_path }}" + owner: "{{ ansible_user }}" + state: directory + become: true + +- name: Clone ovfm-vars generator + git: + repo: 'https://github.com/puiterwijk/qemu-ovmf-secureboot' + dest: "{{ libvirt_ovmf_vars_generator_checkout_path }}" + update: yes + +- name: Get checksum of template OVMF vars + # We need to keep the generated vars in sync with templated version. + # if the OVMF package is updated - we should update a new version with + # the signing keys enrolled. + stat: + path: "{{ libvirt_vm_ovmf_efi_variable_store_path }}" + get_checksum: true + checksum_algorithm: sha256 + register: ovmf_template + +- name: Register destination of generated variables + set_fact: + ovmf_enrolled_variables_path: "\ + {{ libvirt_ovmf_vars_generator_output_path }}/\ + {{ libvirt_ovmf_vars_generator_output_prefix }}\ + {{ ovmf_template.stat.checksum }}" + +- name: Register temporary path to output generated variables + # We don't want to run the generator with elevated privileges + # so use a temporary output before copying into place + set_fact: + ovmf_enrolled_variables_temp_output_path: "\ + {{ libvirt_ovmf_vars_generator_checkout_path}}/\ + {{ libvirt_ovmf_vars_generator_output_prefix }}\ + {{ ovmf_template.stat.checksum }}" + +- name: Check to see if we have generated these vars before + stat: + path: "{{ ovmf_enrolled_variables_path }}" + register: generated_ovmf + +- name: Run OVMF vars generator + command: > + python {{ libvirt_ovmf_vars_generator_checkout_path}}/ovmf-vars-generator + --ovmf-binary {{ libvirt_vm_ovmf_efi_firmware_path }} + --uefi-shell-iso {{ libvirt_vm_ovmf_uefi_shell_iso_path }} + --ovmf-template-vars {{ libvirt_vm_ovmf_efi_variable_store_path }} + --qemu-binary {{ libvirt_vm_emulator }} + {% if libvirt_vm_engine == 'kvm' %}--enable-kvm{% endif %} + --skip-testing + --no-download + {{ ovmf_enrolled_variables_temp_output_path }} + when: not generated_ovmf.stat.exists + +- name: Ensure libvirt qemu can access the variable template + copy: + src: "{{ ovmf_enrolled_variables_temp_output_path }}" + dest: "{{ ovmf_enrolled_variables_path }}" + owner: "{{ libvirt_vm_qemu_user }}" + group: "{{ libvirt_vm_qemu_user }}" + become: true + when: not generated_ovmf.stat.exists diff --git a/tasks/vm.yml b/tasks/vm.yml index eff1152..6bf940d 100644 --- a/tasks/vm.yml +++ b/tasks/vm.yml @@ -9,12 +9,26 @@ skip: true tags: vars +- include_tasks: autodetect.yml + # We include this here as detection depends on the emulation features that + # are requested. We don't need to know the engine and emulator if we're + # not creating any newv VMs. + when: >- + (libvirt_vms | selectattr('state', 'defined') + | selectattr('state', 'equalto', 'absent') | list) != libvirt_vms + +- name: Set fact containing libvirt emulator + set_fact: + # The detected emulator may change between VM, but always prefer an explictly + # requested emulator + emulator: "{{ libvirt_vm_emulator | default(detected_libvirt_vm_emulator, true) }}" + - name: Ensure the VM console log directory exists file: path: "{{ console_log_path | dirname }}" state: directory - owner: "{{ libvirt_vm_log_owner }}" - group: "{{ libvirt_vm_log_owner }}" + owner: "{{ libvirt_vm_qemu_user }}" + group: "{{ libvirt_vm_qemu_user }}" recurse: true mode: 0770 when: console_log_enabled | bool @@ -26,6 +40,26 @@ interface: "{{ item }}" with_items: "{{ interfaces }}" +- name: Create secure boot template variables + include_tasks: prepare-secure-boot.yml + when: + - boot_firmware == "efi" + - enable_secure_boot + - libvirt_vm_ovmf_uefi_shell_iso_path is defined + +- name: Undefine libvirt machine if already defined + # Otherwise vm.xml.j2 will not be updated when variables are changed + # + # note(wszumski): the virt module does not seem to support + # removing vms with nvram defined - as a workaround, use the + # virsh cli directly. It may be better to detect if dumpxml + # actually contains an nvram element rather than relying on + # boot_firmware having the correct value. + command: virsh -c qemu:///system undefine{% if boot_firmware == 'efi' %} --nvram{% endif %} {{ vm.name }} + become: true + register: result + failed_when: result.rc != 0 and "no domain with matching name" not in result.stderr + - name: Ensure the VM is defined virt: name: "{{ vm.name }}" diff --git a/templates/vm.xml.j2 b/templates/vm.xml.j2 index 987c147..97cbfa6 100644 --- a/templates/vm.xml.j2 +++ b/templates/vm.xml.j2 @@ -12,12 +12,26 @@ + {% if boot_firmware == "efi" %} + {# NOTE: pflash requires qemu 1.6 or newer. There are alternatives for older versions, but + they do not work with secure boot. See OVMF readme for an overview #} + {{ libvirt_vm_ovmf_efi_firmware_path }} + + {% endif %} + + {% if enable_feature_acpi %} + + {% endif %} + {% if enable_feature_smm %} + + {% endif %} + - {{ libvirt_vm_emulator }} + {{ emulator }} {% for volume in volumes %} diff --git a/vars/Debian.yml b/vars/Debian.yml index 3dda5d3..394d226 100644 --- a/vars/Debian.yml +++ b/vars/Debian.yml @@ -1,7 +1,16 @@ --- -# Who owns the serial console logs in console_log_path -libvirt_vm_log_owner: libvirt-qemu +# This controls ownership of files used by the libvirt qemu process, +# e.g the serial console logs in console_log_path. +libvirt_vm_qemu_user: libvirt-qemu # The environment passed to virt_volume.sh libvirt_vm_volume_creation_env: {} + +# Path to template OVMF efi variable store. A copy will be created +# for each VM created. +libvirt_vm_ovmf_efi_variable_store_path: /usr/share/OVMF/OVMF_VARS.fd + +# Path to OVMF efi firmware +libvirt_vm_ovmf_efi_firmware_path: /usr/share/OVMF/OVMF_CODE.fd + diff --git a/vars/RedHat.yml b/vars/RedHat.yml index aa0640e..1489c04 100644 --- a/vars/RedHat.yml +++ b/vars/RedHat.yml @@ -1,9 +1,34 @@ --- -# Who owns the serial console logs in console_log_path -libvirt_vm_log_owner: qemu +# This controls ownership of files used by the libvirt qemu process, +# e.g the serial console logs in console_log_path. +libvirt_vm_qemu_user: qemu # The environment passed to virt_volume.sh libvirt_vm_volume_creation_env: VOLUME_GROUP: qemu VOLUME_OWNER: qemu + +# Path to template OVMF efi variable store. A copy will be created +# for each VM created. +# note(wszumski): official package path is /usr/share/OVMF/OVMF_VARS.fd +libvirt_vm_ovmf_efi_variable_store_path: >- + {%- if enable_feature_smm -%} + /usr/share/edk2.git/ovmf-x64/OVMF_VARS-need-smm.fd + {%- else -%} + /usr/share/edk2.git/ovmf-x64/OVMF_VARS-pure-efi.fd + {%- endif -%} + +# Path to OVMF efi firmware +# note(wszumski): official package path is /usr/share/OVMF/OVMF_CODE.secboot.fd +libvirt_vm_ovmf_efi_firmware_path: >- + {%- if enable_feature_smm -%} + /usr/share/edk2.git/ovmf-x64/OVMF_CODE-need-smm.fd + {%- else -%} + /usr/share/edk2.git/ovmf-x64/OVMF_CODE-pure-efi.fd + {%- endif -%} + +# Path to iso containing signing keys +# note(wszumski): official package path is /usr/share/OVMF/UefiShell.iso +libvirt_vm_ovmf_uefi_shell_iso_path: /usr/share/edk2.git/ovmf-x64/UefiShell.iso +