blob: ddcc0a71f18692fad748bec715db9a38fe5ac09b [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"
)
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 Device struct {
Model string
options []string
}
type DeviceModel string
// DeviceModel constants.
const (
DeviceModelVirtioBlkPCI = "virtio-blk-pci"
DeviceModelVirtioNetPCI = "virtio-net-pci"
)
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
}
// TargetEnum provides accessors to valid QEMU target strings.
var TargetEnum = &targetList{
AArch64: "aarch64",
X86_64: "x86_64",
}
// 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
kernelArgs []string
// 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
}
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("-cpu", "max")
}
case TargetEnum.X86_64:
q.SetFlag("-machine", "q35")
if kvm {
q.SetFlag("-cpu", "host")
q.SetFlag("-enable-kvm")
} else {
q.SetFlag("-cpu", "Haswell,+smap,-check,-fsgsbase")
}
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) {
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", 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) 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.qemuPath == "" {
return fmt.Errorf("QEMU binary path must be set.")
}
if q.kernel == "" {
return fmt.Errorf("QEMU kernel path must be set.")
}
if q.initrd == "" {
return fmt.Errorf("QEMU initrd path must be set.")
}
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 err := q.validate(); err != nil {
return []string{}, err
}
cmd := []string{
q.qemuPath,
"-kernel", q.kernel,
"-initrd", q.initrd,
}
cmd = append(cmd, q.args...)
// Treat the absense of specified networks as a directive to disable networking entirely.
if !q.hasNetwork {
cmd = append(cmd, "-net", "none")
}
if len(q.kernelArgs) > 0 {
cmd = append(cmd, "-append", strings.Join(q.kernelArgs, " "))
}
return cmd, nil
}
func (q *QEMUCommandBuilder) recordError(err error) {
q.errs = append(q.errs, err.Error())
}