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
+