builder: add EFI-specific boot values

BIOS and (U)EFI are two standard for booting machines. BIOS being
legacy, which is still supported by several OSes, but is getting phased
out little-by-little, in favour of EFI.

EFI requires several components to boot a machine: a GPT-backed disk, a
firmware to load the bootloaders, and efivars to keep persistent data in
NVRAM between boots.

This is translated in qemu as a collection of a code file that contains
the firmware, and a vars file that holds a template efivars, which may
have some keys or properties setup.

This commit adds new configuration values to enable this. At its
simplest, this might be just enabling `efi_enabled` to boot with EFI
support.

In practice, YMMV, as some protocols (typically Secure Boot) will
require more setup before being able to get something to work as
expected.

The EFI CODE and VARS files are automatically loaded from
/usr/share/OVMF if available, otherwise they need to be specified
manually.

The docs for Qemu are updated to reflect these changes.
This commit is contained in:
Lucas Bajolet 2022-10-17 16:26:43 -04:00 committed by Lucas Bajolet
parent 2348cbe3d0
commit 4076d6da34
12 changed files with 264 additions and 6 deletions

View File

@ -111,6 +111,11 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook)
NetBridge: b.config.NetBridge,
},
new(stepConfigureVNC),
&stepPrepareEfivars{
EFIEnabled: b.config.QemuEFIBootConfig.EnableEFI,
OutputDir: b.config.OutputDir,
SourcePath: b.config.QemuEFIBootConfig.OVMFVars,
},
&stepRun{
DiskImage: b.config.DiskImage,
},

View File

@ -161,6 +161,50 @@ func (c QemuSMPConfig) getMaxCPUs() int {
return totalVCPUs
}
// Booting in EFI mode
//
// Use these options if wanting to boot on a UEFI firmware, as the options to
// do so are different from what BIOS (default) booting will require.
type QemuEFIBootConfig struct {
// Boot in EFI mode instead of BIOS. This is required for more modern
// guest OS. If either or both of `efi_firmware_code` or
// `efi_firmware_vars` are defined, this will implicitely be set to `true`.
//
// NOTE: when using a Secure-Boot enabled firmware, the machine type has
// to be q35, otherwise qemu will not boot.
EnableEFI bool `mapstructure:"efi_boot" required:"false"`
// Path to the CODE part of OVMF (or other compatible firmwares)
// The OVMF_CODE.fd file contains the bootstrap code for booting in EFI
// mode, and requires a separate VARS.fd file to be able to persist data
// between boot cycles.
//
// Default: /usr/share/OVMF/OVMF_CODE.fd
OVMFCode string `mapstructure:"efi_firmware_code" required:"false"`
// Path to the VARS corresponding to the OVMF code file.
//
// Default: /usr/share/OVMF/OVMF_VARS.fd
OVMFVars string `mapstructure:"efi_firmware_vars" required:"false"`
}
func (efiCfg *QemuEFIBootConfig) loadDefaults() {
// Auto enable EFI if either of the Code/Vars path is set
if efiCfg.OVMFCode != "" || efiCfg.OVMFVars != "" {
efiCfg.EnableEFI = true
}
if !efiCfg.EnableEFI {
return
}
if efiCfg.OVMFCode == "" {
efiCfg.OVMFCode = "/usr/share/OVMF/OVMF_CODE.fd"
}
if efiCfg.OVMFVars == "" {
efiCfg.OVMFVars = "/usr/share/OVMF/OVMF_VARS.fd"
}
}
type Config struct {
common.PackerConfig `mapstructure:",squash"`
commonsteps.HTTPConfig `mapstructure:",squash"`
@ -171,6 +215,7 @@ type Config struct {
commonsteps.FloppyConfig `mapstructure:",squash"`
commonsteps.CDConfig `mapstructure:",squash"`
QemuSMPConfig `mapstructure:",squash"`
QemuEFIBootConfig `mapstructure:",squash"`
// Use iso from provided url. Qemu must support
// curl block device. This defaults to `false`.
ISOSkipCache bool `mapstructure:"iso_skip_cache" required:"false"`
@ -203,17 +248,21 @@ type Config struct {
// Each additional disk uses the same disk parameters as the default disk.
// Unset by default.
AdditionalDiskSize []string `mapstructure:"disk_additional_size" required:"false"`
// The firmware file to be used by QEMU
// this option could be set to use EFI instead of BIOS,
// by using "OVMF.fd" from OpenFirmware, for example.
// The firmware file to be used by QEMU.
// If unset, QEMU will load its default firmware.
// Also see the QEMU documentation.
//
// NOTE: when booting in UEFI mode, please use the `efi_` options to
// setup the firmware.
Firmware string `mapstructure:"firmware" required:"false"`
// If a firmware file option was provided, this option can be
// used to change how qemu will get it.
// If false (the default), then the firmware is provided through
// the -bios option, but if true, a pflash drive will be used
// instead.
//
// NOTE: when booting in UEFI mode, please use the `efi_` options to
// setup the firmware.
PFlash bool `mapstructure:"use_pflash" required:"false"`
// The interface to use for the disk. Allowed values include any of `ide`,
// `sata`, `scsi`, `virtio` or `virtio-scsi`^\*. Note also that any boot
@ -288,6 +337,11 @@ type Config struct {
// The type of machine emulation to use. Run your qemu binary with the
// flags `-machine help` to list available types for your system. This
// defaults to `pc`.
//
// NOTE: when booting a UEFI machine with Secure Boot enabled, this has
// to be a q35 derivative.
// If the machine is not a q35 derivative, nothing will boot (not even
// an EFI shell).
MachineType string `mapstructure:"machine_type" required:"false"`
// The amount of memory to use when building the VM
// in megabytes. This defaults to 512 megabytes.
@ -646,6 +700,8 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
c.TPMType = "tpm-tis"
}
c.QemuEFIBootConfig.loadDefaults()
errs = packersdk.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...)
errs = packersdk.MultiErrorAppend(errs, c.CDConfig.Prepare(&c.ctx)...)
errs = packersdk.MultiErrorAppend(errs, c.VNCConfig.Prepare(&c.ctx)...)

View File

@ -101,6 +101,9 @@ type FlatConfig struct {
SocketCount *int `mapstructure:"sockets" required:"false" cty:"sockets" hcl:"sockets"`
CoreCount *int `mapstructure:"cores" required:"false" cty:"cores" hcl:"cores"`
ThreadCount *int `mapstructure:"threads" required:"false" cty:"threads" hcl:"threads"`
EnableEFI *bool `mapstructure:"efi_boot" required:"false" cty:"efi_boot" hcl:"efi_boot"`
OVMFCode *string `mapstructure:"efi_firmware_code" required:"false" cty:"efi_firmware_code" hcl:"efi_firmware_code"`
OVMFVars *string `mapstructure:"efi_firmware_vars" required:"false" cty:"efi_firmware_vars" hcl:"efi_firmware_vars"`
ISOSkipCache *bool `mapstructure:"iso_skip_cache" required:"false" cty:"iso_skip_cache" hcl:"iso_skip_cache"`
Accelerator *string `mapstructure:"accelerator" required:"false" cty:"accelerator" hcl:"accelerator"`
AdditionalDiskSize []string `mapstructure:"disk_additional_size" required:"false" cty:"disk_additional_size" hcl:"disk_additional_size"`
@ -246,6 +249,9 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"sockets": &hcldec.AttrSpec{Name: "sockets", Type: cty.Number, Required: false},
"cores": &hcldec.AttrSpec{Name: "cores", Type: cty.Number, Required: false},
"threads": &hcldec.AttrSpec{Name: "threads", Type: cty.Number, Required: false},
"efi_boot": &hcldec.AttrSpec{Name: "efi_boot", Type: cty.Bool, Required: false},
"efi_firmware_code": &hcldec.AttrSpec{Name: "efi_firmware_code", Type: cty.String, Required: false},
"efi_firmware_vars": &hcldec.AttrSpec{Name: "efi_firmware_vars", Type: cty.String, Required: false},
"iso_skip_cache": &hcldec.AttrSpec{Name: "iso_skip_cache", Type: cty.Bool, Required: false},
"accelerator": &hcldec.AttrSpec{Name: "accelerator", Type: cty.String, Required: false},
"disk_additional_size": &hcldec.AttrSpec{Name: "disk_additional_size", Type: cty.List(cty.String), Required: false},

View File

@ -0,0 +1,72 @@
package qemu
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
// stepPrepareEfivars copies the EFIVars file to the output, so we can boot
// and use it as a RW flash drive
type stepPrepareEfivars struct {
EFIEnabled bool
OutputDir string
SourcePath string
}
const efivarStateKey string = "EFI_VARS_FILE_PATH"
func (s *stepPrepareEfivars) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packersdk.Ui)
if !s.EFIEnabled {
return multistep.ActionContinue
}
dstPath := filepath.Join(s.OutputDir, "efivars.fd")
outFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY, 0660)
if err != nil {
errMsg := fmt.Sprintf("failed to create local efivars file at %s: %s", dstPath, err)
ui.Error(errMsg)
return multistep.ActionHalt
}
defer outFile.Close()
state.Put(efivarStateKey, dstPath)
inFile, err := os.Open(s.SourcePath)
if err != nil {
errMsg := fmt.Sprintf("failed to read from efivars file at %s: %s", s.SourcePath, err)
ui.Error(errMsg)
return multistep.ActionHalt
}
_, err = io.Copy(outFile, inFile)
if err != nil {
errMsg := fmt.Sprintf("failed to copy efivars data: %s", err)
ui.Error(errMsg)
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepPrepareEfivars) Cleanup(state multistep.StateBag) {
if !s.EFIEnabled {
return
}
efiVarFile, ok := state.GetOk(efivarStateKey)
// If the path isn't in state, we can assume it's not been created and
// therefore we have nothing to cleanup
if !ok {
return
}
os.Remove(efiVarFile.(string))
}

View File

@ -287,11 +287,21 @@ func (s *stepRun) getDeviceAndDriveArgs(config *Config, state multistep.StateBag
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,index=%d,id=cdrom%d,media=cdrom", cdPath, config.CDROMInterface, i, i))
}
}
// Firmware
if config.Firmware != "" && config.PFlash {
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=pflash,format=raw,readonly=on", config.Firmware))
}
// EFI
if config.QemuEFIBootConfig.EnableEFI {
// CODE binary is loaded readonly
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=pflash,unit=0,format=raw,readonly=on", config.QemuEFIBootConfig.OVMFCode))
efivar := state.Get(efivarStateKey)
// the local copy of VARS is not
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=pflash,unit=1,format=raw", efivar.(string)))
}
// TPM
if config.VTPM {
deviceArgs = append(deviceArgs, fmt.Sprintf("%s,tpmdev=tpm0", config.TPMType))

View File

@ -32,17 +32,21 @@
Each additional disk uses the same disk parameters as the default disk.
Unset by default.
- `firmware` (string) - The firmware file to be used by QEMU
this option could be set to use EFI instead of BIOS,
by using "OVMF.fd" from OpenFirmware, for example.
- `firmware` (string) - The firmware file to be used by QEMU.
If unset, QEMU will load its default firmware.
Also see the QEMU documentation.
NOTE: when booting in UEFI mode, please use the `efi_` options to
setup the firmware.
- `use_pflash` (bool) - If a firmware file option was provided, this option can be
used to change how qemu will get it.
If false (the default), then the firmware is provided through
the -bios option, but if true, a pflash drive will be used
instead.
NOTE: when booting in UEFI mode, please use the `efi_` options to
setup the firmware.
- `disk_interface` (string) - The interface to use for the disk. Allowed values include any of `ide`,
`sata`, `scsi`, `virtio` or `virtio-scsi`^\*. Note also that any boot
@ -117,6 +121,11 @@
- `machine_type` (string) - The type of machine emulation to use. Run your qemu binary with the
flags `-machine help` to list available types for your system. This
defaults to `pc`.
NOTE: when booting a UEFI machine with Secure Boot enabled, this has
to be a q35 derivative.
If the machine is not a q35 derivative, nothing will boot (not even
an EFI shell).
- `memory` (int) - The amount of memory to use when building the VM
in megabytes. This defaults to 512 megabytes.

View File

@ -0,0 +1,18 @@
<!-- Code generated from the comments of the QemuEFIBootConfig struct in builder/qemu/config.go; DO NOT EDIT MANUALLY -->
- `efi_boot` (bool) - Boot in EFI mode instead of BIOS. This is required for more modern
guest OS. If either or both of `efi_firmware_code` or
`efi_firmware_vars` are defined, this will implicitely be set to `true`.
- `efi_firmware_code` (string) - Path to the CODE part of OVMF (or other compatible firmwares)
The OVMF_CODE.fd file contains the bootstrap code for booting in EFI
mode, and requires a separate VARS.fd file to be able to persist data
between boot cycles.
Default: /usr/share/OVMF/OVMF_CODE.fd
- `efi_firmware_vars` (string) - Path to the VARS corresponding to the OVMF code file.
Default: /usr/share/OVMF/OVMF_VARS.fd
<!-- End of code generated from the comments of the QemuEFIBootConfig struct in builder/qemu/config.go; -->

View File

@ -0,0 +1,8 @@
<!-- Code generated from the comments of the QemuEFIBootConfig struct in builder/qemu/config.go; DO NOT EDIT MANUALLY -->
Booting in EFI mode
Use these options if wanting to boot on a UEFI firmware, as the options to
do so are different from what BIOS (default) booting will require.
<!-- End of code generated from the comments of the QemuEFIBootConfig struct in builder/qemu/config.go; -->

View File

@ -0,0 +1,52 @@
source "qemu" "debian_efi" {
iso_url = "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-11.5.0-amd64-netinst.iso"
iso_checksum = "sha256:e307d0e583b4a8f7e5b436f8413d4707dd4242b70aea61eb08591dc0378522f3"
communicator = "ssh"
ssh_username = "root"
ssh_password = "root"
ssh_timeout = "30m"
output_directory = "./out"
memory = "1024"
disk_size = "6G"
cpus = 4
format = "qcow2"
accelerator = "kvm"
vm_name = "debian_efi"
# headless = "false" # uncomment to see the boot process in a qemu window
machine_type = "q35" # As of now, q35 is required for secure boot to be enabled
boot_command = [
"<enter>FS0:<enter>EFI\\boot\\bootx64.efi<enter>",
"<wait><down><down><enter>", # manual install
"<wait><down><down><down><down><down><enter>", # automatic install
"<wait30>", # wait 30s for preseed prompt
"http://{{.HTTPIP}}:{{.HTTPPort}}/preseed.cfg<tab><enter>",
"<wait><enter>", # select English as language/locale
"<wait><enter>", # select English as language
"<wait><enter>", # set English-US as keyboard layout
"<wait><wait><wait>root<enter>", # set root password
"<wait>root<enter>", # confirm root password
"<wait>debian<enter>", # set machine name to debian
"<wait><enter>", # set user to debian
"<wait>debian<enter>", # set password to debian
"<wait>debian<enter>", # confirm password to debian
"<wait180>", # wait 3m for system to install
"root<enter>root<enter>sed -Ei 's/^#.*PermitRootLogin.*$/PermitRootLogin yes/' /etc/ssh/sshd_config<enter>systemctl restart sshd<enter>exit<enter>" # configure sshd to allow root connection
]
http_directory = "http"
boot_wait = "3s"
qemuargs = [
["-cpu", "host"],
["-vga","virtio"] # if vga is not virtio, output is garbled for some reason
]
vtpm = true
efi_firmware_code = "./efi_data/OVMF_CODE_4M.ms.fd"
efi_firmware_vars = "./efi_data/OVMF_VARS_4M.ms.fd"
}
build {
sources = ["source.qemu.debian_efi"]
provisioner "shell" {
inline = [ "dmesg | grep -qi 'Secure boot enabled' && echo \"Secure Boot is on!\"" ]
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,22 @@
choose-mirror-bin mirror/http/proxy string
d-i debian-installer/framebuffer boolean false
d-i debconf/frontend select noninteractive
d-i base-installer/kernel/override-image string linux-server
d-i clock-setup/utc boolean true
d-i clock-setup/utc-auto boolean true
d-i finish-install/reboot_in_progress note
d-i grub-installer/only_debian boolean true
d-i grub-installer/with_other_os boolean true
d-i partman-auto/method string regular
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
d-i partman/confirm_write_new_label boolean true
d-i pkgsel/include string openssh-server
d-i pkgsel/install-language-support boolean false
d-i pkgsel/update-policy select none
d-i pkgsel/upgrade select full-upgrade
d-i time/zone string UTC
d-i user-setup/allow-password-weak boolean true
d-i user-setup/encrypt-home boolean false
tasksel tasksel/first multiselect standard, ubuntu-server