Skip to content

WIP: Add efi and secureboot support #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
13 changes: 13 additions & 0 deletions defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
48 changes: 30 additions & 18 deletions tasks/autodetect.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this not set it to the string 'none'? You can set a variable to none via:

detected_libvirt_vm_emulator:


- include_tasks: detect-kvm.yml
when: libvirt_vm_engine == 'kvm'

- block:
Expand All @@ -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
9 changes: 6 additions & 3 deletions tasks/destroy-vm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit clunky :( Looks like it's necessary though

become: yes
when: vm.name in result.list_vms
16 changes: 16 additions & 0 deletions tasks/detect-kvm.yml
Original file line number Diff line number Diff line change
@@ -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
23 changes: 16 additions & 7 deletions tasks/main.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -18,14 +12,27 @@
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) }}"
volumes: "{{ vm.volumes | default([], true) }}"
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
Expand All @@ -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
Expand Down
107 changes: 107 additions & 0 deletions tasks/prepare-secure-boot.yml
Original file line number Diff line number Diff line change
@@ -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)/[email protected].
# 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)/[email protected]
#
# 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
38 changes: 36 additions & 2 deletions tasks/vm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we want to do this unconditionally?


- 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
Expand All @@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not supported on Debian?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Debian packages don't contain the UEFI shell ISO which contains the EnrollDefaultKeys.efi binary. WE would either need to build this from source or obtain it by some other means.


- 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 }}"
Expand Down
16 changes: 15 additions & 1 deletion templates/vm.xml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,26 @@
<boot dev='hd'/>
<boot dev='network'/>
<bios useserial='yes'/>
{% 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 #}
<loader readonly='yes' type='pflash'>{{ libvirt_vm_ovmf_efi_firmware_path }}</loader>
<nvram template='{{ ovmf_enrolled_variables_path | default(libvirt_vm_ovmf_efi_variable_store_path) }}'/>
{% endif %}
</os>
<features>
{% if enable_feature_acpi %}
<acpi/>
{% endif %}
{% if enable_feature_smm %}
<smm/>
{% endif %}
</features>
<cpu{% if cpu_mode is not none %} mode='{{ cpu_mode }}'{% endif %}>
<model fallback='allow'/>
</cpu>
<devices>
<emulator>{{ libvirt_vm_emulator }}</emulator>
<emulator>{{ emulator }}</emulator>
{% for volume in volumes %}
<disk type='volume' device='{{ volume.device | default(libvirt_volume_default_device) }}'>
<driver name='qemu' type='{{ volume.format | default(libvirt_volume_default_format) }}'/>
Expand Down
13 changes: 11 additions & 2 deletions vars/Debian.yml
Original file line number Diff line number Diff line change
@@ -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

Loading