blob: 44d7823925e512df03363b88aaf187ad2669e04f [file] [log] [blame]
// Copyright 2018 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 main
import (
"context"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"fuchsia.googlesource.com/infra/infra/qemu"
"fuchsia.googlesource.com/infra/infra/secrets"
"github.com/google/subcommands"
)
// QCOWImageName is a default name for a QEMU CoW (Copy on Write) image.
const qcowImageName = "fuchsia.qcow2"
// qemuImgTool is the name of the QEMU image utility tool.
const qemuImgTool = "qemu-img"
// QEMUBinPrefix is the prefix of the QEMU binary name, which is of the form
// qemu-system-<QEMU arch suffix>.
const qemuBinPrefix = "qemu-system"
// TargetToQEMUArch maps the fuchsia shorthand of a target architecture to the name
// recognized by QEMU.
var targetToQEMUArch = map[string]string{
"x64": "x86_64",
"arm64": "aarch64",
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
return err
}
// OverwriteFileWithCopy overwrites a given file with a copy of it.
//
// This is used to break any hard linking that might back the provided MinFS image.
// For example, Swarming hard links cached isolate downloads - and modifying those will
// modify the cache contents themselves.
func overwriteFileWithCopy(filepath string) error {
copy, err := ioutil.TempFile("", "file-copy")
if err != nil {
return err
}
if err = copyFile(filepath, copy.Name()); err != nil {
return err
}
if err = os.Remove(filepath); err != nil {
return err
}
return os.Rename(copy.Name(), filepath)
}
// QEMUCommand is a Command implementation for running the testing workflow on an emulated
// target within QEMU.
type QEMUCommand struct {
// QEMUBinDir is a path to a directory of QEMU binaries.
qemuBinDir string
// QEMUKernelImage is a path to a qemu-kernel image.
qemuKernelImage string
// zirconAImage is a path to a zircon-a image.
zirconAImage string
// storageFullImage is a path to a storage-full image.
storageFullImage string
// MinFSImage is a path to a minFS image to be mounted on target, and to where test
// results will be written.
minFSImage string
// MinFSBlkDevPCIAddr is a minFS block device PCI address.
minFSBlkDevPCIAddr string
// TargetArch is the target architecture to be emulated within QEMU
targetArch string
// EnableKVM dictates whether to enable KVM.
enableKVM bool
// EnableNetworking dictates whether to enable external networking.
enableNetworking bool
}
func (*QEMUCommand) Name() string {
return "qemu"
}
func (*QEMUCommand) Usage() string {
return "qemu [flags...] [kernel command-line arguments...]\n\nflags:\n"
}
func (*QEMUCommand) Synopsis() string {
return "boots a QEMU device with a MinFS image as a block device."
}
func (cmd *QEMUCommand) SetFlags(f *flag.FlagSet) {
f.StringVar(&cmd.qemuBinDir, "qemu-dir", "", "")
f.StringVar(&cmd.qemuKernelImage, "qemu-kernel", "", "path to a qemu-kernel image")
f.StringVar(&cmd.zirconAImage, "zircon-a", "", "path to a zircon-a image")
f.StringVar(&cmd.storageFullImage, "storage-full", "", "path to a storage-full image")
f.StringVar(&cmd.minFSImage, "minfs", "", "path to minFS image")
f.StringVar(&cmd.minFSBlkDevPCIAddr, "pci-addr", "06.0", "minFS block device PCI address")
f.StringVar(&cmd.targetArch, "arch", "", "target architecture (x64 or arm64)")
f.BoolVar(&cmd.enableKVM, "use-kvm", false, "whether to enable KVM")
f.BoolVar(&cmd.enableNetworking, "enable-networking", false, "whether to enable external networking")
}
// EnsureFlagsAreSet validates that required flags are set and that the provided images exist.
func (cmd *QEMUCommand) validateFlags() error {
var errs []string
if cmd.qemuBinDir == "" {
errs = append(errs, "-qemu-dir must be set")
}
_, ok := targetToQEMUArch[cmd.targetArch]
if !ok {
errs = append(errs, fmt.Sprintf("invalid target architecture: %s", cmd.targetArch))
}
existsIfSet := func(filename string) {
if filename == "" {
return
}
_, err := os.Stat(filename)
if err != nil {
errs = append(errs, fmt.Sprintf("failed to stat %s: %v", filename, err))
}
}
existsIfSet(cmd.qemuKernelImage)
if cmd.qemuKernelImage == "" {
errs = append(errs, "-qemu-kernel must be set.")
}
existsIfSet(cmd.zirconAImage)
if cmd.zirconAImage == "" {
errs = append(errs, "-zircon-a must be set.")
}
existsIfSet(cmd.storageFullImage)
existsIfSet(cmd.minFSImage)
if len(errs) > 0 {
errs = append(errs, "run `help qemu` for flag documentation.")
return errors.New(strings.Join(errs, "\n"))
}
return nil
}
func (cmd *QEMUCommand) execute(ctx context.Context, cmdlineArgs []string) error {
if err := cmd.validateFlags(); err != nil {
return err
}
qemuArch, _ := targetToQEMUArch[cmd.targetArch]
qemuBinPath := filepath.Join(cmd.qemuBinDir, fmt.Sprintf("%s-%s", qemuBinPrefix, qemuArch))
var q qemu.QEMUBuilder
if err := q.Initialize(qemuBinPath, cmd.targetArch, cmd.enableKVM); err != nil {
return err
}
q.AddArgs("-m", "4096")
q.AddArgs("-smp", "4")
q.AddArgs("-nographic")
q.AddArgs("-serial", "stdio")
q.AddArgs("-monitor", "none")
if !cmd.enableNetworking {
q.AddArgs("-net", "none")
}
// The system will halt on a kernel panic instead of rebooting
cmdlineArgs = append(cmdlineArgs, "kernel.halt-on-panic=true")
// Print a message if `dm poweroff` times out.
cmdlineArgs = append(cmdlineArgs, "devmgr.suspend-timeout-debug=true")
// Do not print colors.
cmdlineArgs = append(cmdlineArgs, "TERM=dumb")
if cmd.targetArch == "x64" {
// Necessary to redirect to stdout.
cmdlineArgs = append(cmdlineArgs, "kernel.serial=legacy")
}
q.AddArgs("-append", strings.Join(cmdlineArgs, " "))
// Absolutize qemuKernelImage, zirconAImage, and minFSImage paths so that QEMU can be
// invoked in any working directory.
absQEMUKernelImage, err := filepath.Abs(cmd.qemuKernelImage)
if err != nil {
return err
}
q.AddArgs("-kernel", absQEMUKernelImage)
absZirconAImage, err := filepath.Abs(cmd.zirconAImage)
if err != nil {
return err
}
q.AddArgs("-initrd", absZirconAImage)
if cmd.minFSImage != "" {
absMinFSImage, err := filepath.Abs(cmd.minFSImage)
if err != nil {
return err
}
// See function documentation for the reason behind this step.
if err := overwriteFileWithCopy(absMinFSImage); err != nil {
return err
}
q.AddArgs("-drive", fmt.Sprintf("file=%s,format=raw,if=none,id=testdisk", absMinFSImage))
q.AddArgs("-device", fmt.Sprintf("virtio-blk-pci,drive=testdisk,addr=%s", cmd.minFSBlkDevPCIAddr))
}
if cmd.storageFullImage != "" {
qemuImgToolPath := filepath.Join(cmd.qemuBinDir, qemuImgTool)
qcowDir, err := ioutil.TempDir("", "qcow-dir")
if err != nil {
return err
}
defer os.RemoveAll(qcowDir)
qcowImage := filepath.Join(qcowDir, qcowImageName)
if err = qemu.CreateQCOWImage(qemuImgToolPath, cmd.storageFullImage, qcowImage); err != nil {
return err
}
qcowPath := filepath.Join(qcowDir, qcowImageName)
q.AddArgs("-drive", fmt.Sprintf("file=%s,format=qcow2,if=none,id=maindisk", qcowPath))
q.AddArgs("-device", "virtio-blk-pci,drive=maindisk")
}
// The QEMU command needs to be invoked within an empty directory, as QEMU will attempt
// to pick up files from its working directory, one notable culprit being multiboot.bin.
// This can result in strange behavior.
qemuWorkingDir, err := ioutil.TempDir("", "qemu-working-dir")
if err != nil {
return err
}
defer os.RemoveAll(qemuWorkingDir)
qemuCmd := q.Cmd(ctx)
qemuCmd.Dir = qemuWorkingDir
qemuCmd.Stdout = os.Stdout
qemuCmd.Stderr = os.Stderr
log.Printf("running:\n\tArgs: %s\n\tEnv: %s", qemuCmd.Args, qemuCmd.Env)
return qemu.CheckExitCode(qemuCmd.Run())
}
func (cmd *QEMUCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
// TODO(IN-607) Once the secrets pipeline is supported on hardware, move the starting
// of the secrets server to botanist's main().
//
// The secrets server will start up iff LUCI_CONTEXT is set and contains secret bytes.
secrets.StartSecretsServer(ctx, 8081)
if err := cmd.execute(ctx, f.Args()); err != nil {
log.Print(err)
return subcommands.ExitFailure
}
return subcommands.ExitSuccess
}