blob: 60b8dad9e761a38d6365f04eb3214da790d1ff6d [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package qemu
import (
"errors"
"fmt"
"strings"
)
const (
DefaultNetwork = "10.0.2.0/24"
DefaultDHCPStart = "10.0.2.15"
DefaultGateway = "10.0.2.2"
DefaultDNS = "10.0.2.3"
)
// Config holds emulator configuration values which can be passed to
// `ffx emu start` as a file through the --config flag.
type Config struct {
Args []string `json:"args"`
Envs map[string]string `json:"envs"`
Features []string `json:"features"`
KernelArgs []string `json:"kernel_args"`
Options []string `json:"options"`
}
type Drive struct {
// TODO(kjharland): Embed `Device` here.
// ID is the block device identifier.
ID string
// File is the disk image file.
File string
// Addr is the PCI address of the block device.
Addr string
}
type Chardev struct {
// ID is the character device identifier.
ID string
// Logfile is a path to write the output to.
Logfile string
// Signal controls whether signals are enabled on the terminal.
Signal bool
}
type Forward struct {
// HostPort is the port on the host.
HostPort int
// GuestPort is the port on the guest.
GuestPort int
}
type Netdev struct {
Device
// User is a netdev user backend.
User *NetdevUser
// Tap is a netdev tap backend.
Tap *NetdevTap
// ID is the network device identifier.
ID string
}
type RNGdev struct {
Device
// ID is the RNG device identifier.
ID string
// Filename is the file name of the RNG.
Filename string
}
type Device struct {
Model string
options []string
}
type DeviceModel string
// DeviceModel constants.
const (
DeviceModelVirtioBlkPCI = "virtio-blk-pci"
DeviceModelVirtioNetPCI = "virtio-net-pci"
DeviceModelRNG = "virtio-rng-device"
)
func (d *Device) AddOption(key, val string) {
d.options = append(d.options, fmt.Sprintf("%s=%s", key, val))
}
func (d *Device) String() string {
return fmt.Sprintf("%s,%s", d.Model, strings.Join(d.options, ","))
}
// NetdevUser defines a netdev backend giving user networking.
type NetdevUser struct {
// Network is the network block.
Network string
// DHCPStart is the address at which the DHCP allocation starts.
DHCPStart string
// DNS is the address of the builtin DNS server.
DNS string
// Host is the host IP address.
Host string
// Forwards are the host forwardings.
Forwards []Forward
}
// HCI identifies a Host Controller Interface.
type HCI string
// Host Controller Interface constants.
const (
// XHCI is the Extensible Host Controller Interface.
// https://en.wikipedia.org/wiki/Extensible_Host_Controller_Interface
XHCI = "xhci"
)
// NetdevTap defines a netdev backend giving a tap interface.
type NetdevTap struct {
// Name is the name of the interface.
Name string
}
// Target is a QEMU architecture target.
type Target string
type targetList struct {
AArch64 Target
X86_64 Target
RiscV64 Target
}
// TargetEnum provides accessors to valid QEMU target strings.
var TargetEnum = &targetList{
AArch64: "aarch64",
X86_64: "x86_64",
RiscV64: "riscv64",
}
type uefiVolumes struct {
rom string
nvram string
filesystem string
}
// QEMUCommandBuilder provides methods to construct an arbitrary
// QEMU invocation, it does not validate inputs only that the
// resulting invocation is structurely valid.
type QEMUCommandBuilder struct {
args []string
qemuPath string
hasNetwork bool
initrd string
kernel string
uefi *uefiVolumes
kernelArgs []string
hasDisk bool
forAEMU bool // For AEMU-specific quirks
// Any errors encountered while building the command.
errs []string
}
func (q *QEMUCommandBuilder) SetFlag(args ...string) {
q.args = append(q.args, args...)
}
func (q *QEMUCommandBuilder) SetBinary(qemuPath string) {
q.qemuPath = qemuPath
}
func (q *QEMUCommandBuilder) SetKernel(kernel string) {
q.kernel = kernel
}
func (q *QEMUCommandBuilder) SetInitrd(initrd string) {
q.initrd = initrd
}
// SetUEFIVolumes specifies the two storage volumes necessary for booting
// through UEFI - the ROM image implementing UEFI itself and a NVRAM image
// containing the EFI global variable store - as well as an optional,
// bootable (FAT) filesystem image containing the desired application.
func (q *QEMUCommandBuilder) SetUEFIVolumes(rom, nvram, filesystem string) {
q.uefi = &uefiVolumes{rom, nvram, filesystem}
}
func (q *QEMUCommandBuilder) SetTarget(target Target, kvm bool) {
switch target {
case TargetEnum.AArch64:
if kvm {
q.SetFlag("-machine", "virt-2.12,gic-version=host")
q.SetFlag("-cpu", "host")
q.SetFlag("-enable-kvm")
} else {
q.SetFlag("-machine", "virt-2.12,gic-version=3,virtualization=true")
q.SetFlag("-accel", "tcg,thread=multi")
q.SetFlag("-cpu", "max")
}
case TargetEnum.X86_64:
q.AddKernelArg("kernel.serial=legacy")
// AEMU's fork of QEMU does not support the smbios-entry-point-type
// machine property, but supports the type the kernel does without any
// added qualification.
if q.forAEMU {
q.SetFlag("-machine", "q35")
} else {
q.SetFlag("-machine", "q35,smbios-entry-point-type=32")
}
// Override the SeaBIOS serial port to keep it from outputting
// a terminal reset on start.
q.SetFlag("-fw_cfg", "name=etc/sercon-port,string=0")
if kvm {
q.SetFlag("-cpu", "host,migratable=no,+invtsc")
q.SetFlag("-enable-kvm")
} else {
q.SetFlag("-cpu", "Skylake-Client,-check")
q.SetFlag("-accel", "tcg,thread=multi")
}
case TargetEnum.RiscV64:
if kvm {
q.recordError(fmt.Errorf("KVM is not supported for RISC-V 64 bit yet"))
} else {
q.SetFlag("-cpu", "rv64,svpbmt=true,v=true,vext_spec=v1.0")
q.SetFlag("-machine", "virt")
q.SetFlag("-accel", "tcg,thread=multi")
}
default:
q.recordError(fmt.Errorf("invalid target: %q", target))
}
}
func (q *QEMUCommandBuilder) SetMemory(memoryBytes int) {
q.SetFlag("-m", fmt.Sprintf("%d", memoryBytes))
}
func (q *QEMUCommandBuilder) SetCPUCount(cpuCount int) {
q.SetFlag("-smp", fmt.Sprintf("%d", cpuCount))
}
func (q *QEMUCommandBuilder) AddVirtioBlkPciDrive(d Drive) {
q.hasDisk = true
iothread := fmt.Sprintf("iothread-%s", d.ID)
q.SetFlag("-object", fmt.Sprintf("iothread,id=%s", iothread))
q.SetFlag("-drive", fmt.Sprintf("id=%s,file=%s,format=raw,if=none,cache=unsafe,aio=threads", d.ID, d.File))
device := fmt.Sprintf("virtio-blk-pci,drive=%s,iothread=%s", d.ID, iothread)
if d.Addr != "" {
device += fmt.Sprintf(",addr=%s", d.Addr)
}
q.SetFlag("-device", device)
}
func (q *QEMUCommandBuilder) AddUSBDrive(d Drive) {
q.SetFlag("-drive", fmt.Sprintf("if=none,id=%s,file=%s,format=raw", d.ID, d.File))
q.SetFlag("-device", fmt.Sprintf("usb-storage,drive=%s,removable=on", d.ID))
}
func (q *QEMUCommandBuilder) AddUFSDrive(d Drive) {
q.SetFlag("-device", "ufs")
q.SetFlag("-drive", fmt.Sprintf("if=none,id=%s,file=%s,format=raw,discard=unmap", d.ID, d.File))
q.SetFlag("-device", fmt.Sprintf("ufs-lu,drive=%s,lun=0", d.ID))
}
// AddHCI adds an host-controller-interface.
func (q *QEMUCommandBuilder) AddHCI(hci HCI) {
if hci != XHCI {
q.recordError(fmt.Errorf("unimplemented host controller interface: %q", hci))
return
}
q.SetFlag("-device", "qemu-xhci,id=xhci")
}
func (q *QEMUCommandBuilder) AddSerial(c Chardev) {
var builder strings.Builder
builder.WriteString(fmt.Sprintf("stdio,id=%s", c.ID))
if c.Logfile != "" {
builder.WriteString(fmt.Sprintf(",logfile=%s", c.Logfile))
}
if !c.Signal {
builder.WriteString(",signal=off")
}
q.SetFlag("-chardev", builder.String())
device := fmt.Sprintf("chardev:%s", c.ID)
q.SetFlag("-serial", device)
}
func (q *QEMUCommandBuilder) AddNetwork(n Netdev) {
var network strings.Builder
if n.Tap != nil {
// Overwrite any default configuration scripts with none; there is not currently a
// good use case for these parameters.
network.WriteString(fmt.Sprintf("tap,id=%s,ifname=%s,script=no,downscript=no", n.ID, n.Tap.Name))
} else if n.User != nil {
network.WriteString(fmt.Sprintf("user,id=%s", n.ID))
if n.User.Network != "" {
network.WriteString(fmt.Sprintf(",net=%s", n.User.Network))
}
if n.User.DHCPStart != "" {
network.WriteString(fmt.Sprintf(",dhcpstart=%s", n.User.DHCPStart))
}
if n.User.DNS != "" {
network.WriteString(fmt.Sprintf(",dns=%s", n.User.DNS))
}
if n.User.Host != "" {
network.WriteString(fmt.Sprintf(",host=%s", n.User.Host))
}
for _, f := range n.User.Forwards {
network.WriteString(fmt.Sprintf(",hostfwd=tcp::%d-:%d", f.HostPort, f.GuestPort))
}
}
q.SetFlag("-netdev", network.String())
n.Device.AddOption("netdev", n.ID)
q.SetFlag("-device", n.Device.String())
q.hasNetwork = true
}
func (q *QEMUCommandBuilder) AddRNG(r RNGdev) {
q.SetFlag("-object", fmt.Sprintf("rng-random,filename=%s,id=%s", r.Filename, r.ID))
r.AddOption("rng", r.ID)
q.SetFlag("-device", r.String())
}
func (q *QEMUCommandBuilder) AddKernelArg(kernelArg string) {
q.kernelArgs = append(q.kernelArgs, kernelArg)
}
func (q *QEMUCommandBuilder) validate() error {
if len(q.errs) == 1 {
return errors.New(q.errs[0])
}
if len(q.errs) > 0 {
return fmt.Errorf("multiple errors: [\n%s\n]", strings.Join(q.errs, ",\n"))
}
if q.kernel == "" && q.uefi == nil {
return fmt.Errorf("one of QEMU kernel path or a UEFI disk image must be set.")
}
uefiDiskBoot := q.uefi != nil && q.uefi.filesystem != ""
if uefiDiskBoot && (q.kernel != "" || q.initrd != "") {
return fmt.Errorf("specifying a UEFI filesystem/disk image is mutually exclusive with QEMU kernel and initrd.")
}
return nil
}
// Build creates a QEMU invocation given a particular configuration, a list of
// images, and any specified command-line arguments.
func (q *QEMUCommandBuilder) Build() ([]string, error) {
if q.qemuPath == "" {
return []string{}, fmt.Errorf("QEMU binary path must be set.")
}
config, err := q.BuildConfig()
if err != nil {
return []string{}, err
}
cmd := []string{q.qemuPath}
cmd = append(cmd, config.Args...)
if len(config.KernelArgs) > 0 {
cmd = append(cmd, "-append", strings.Join(config.KernelArgs, " "))
}
return cmd, nil
}
// BuildConfig returns a Config storing the emulator configuration values.
// It can be written to a file and passed through the --config flag to `ffx emu start`.
func (q *QEMUCommandBuilder) BuildConfig() (Config, error) {
var config Config
if err := q.validate(); err != nil {
return config, err
}
config.Envs = make(map[string]string)
config.Features = []string{}
if q.kernel != "" {
config.Args = append(config.Args, "-kernel", q.kernel)
}
if q.initrd != "" {
config.Args = append(config.Args, "-initrd", q.initrd)
}
config.KernelArgs = []string{}
if q.uefi != nil {
config.Args = append(
config.Args,
"-drive",
fmt.Sprintf("if=pflash,format=raw,readonly=on,file=%s", q.uefi.rom),
// `snapshot=true`` ensures that this image is copy-on-write and that
// state does not persist across invocations.
"-drive",
fmt.Sprintf("if=pflash,format=raw,snapshot=on,file=%s", q.uefi.nvram),
)
if q.uefi.filesystem != "" {
config.Args = append(
config.Args, "-drive",
fmt.Sprintf("if=none,format=raw,file=%s,id=uefi", q.uefi.filesystem),
)
// `bootindex=0` ensures that the provided storage device is
// regarded as the highest priority boot option.
bootindex := ""
if q.kernel == "" {
bootindex = ",bootindex=0"
}
// If no disk is present, put the filesystem image on a USB drive.
if q.hasDisk {
config.Args = append(
config.Args,
"-device",
fmt.Sprintf("virtio-blk-pci,drive=uefi%s", bootindex),
)
} else {
config.Args = append(
config.Args,
"-device",
"nec-usb-xhci,id=xhci0",
"-device",
fmt.Sprintf("usb-storage,bus=xhci0.0,drive=uefi,removable=on%s", bootindex),
)
}
}
}
config.Args = append(config.Args, q.args...)
config.Options = []string{}
// Treat the absence of specified networks as a directive to disable networking entirely.
if !q.hasNetwork {
config.Args = append(config.Args, "-net", "none")
}
// `-append` requires `-kernel`.
if q.kernel != "" {
config.KernelArgs = q.kernelArgs
}
return config, nil
}
func (q *QEMUCommandBuilder) recordError(err error) {
q.errs = append(q.errs, err.Error())
}