github.com/oweisse/u-root@v0.0.0-20181109060735-d005ad25fef1/pkg/qemu/qemu.go (about)

     1  // Copyright 2018 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package qemu is suitable for running QEMU-based integration tests.
     6  //
     7  // The environment variable `UROOT_QEMU` overrides the path to QEMU and the
     8  // first few arguments (defaults to "qemu"). For example, I use:
     9  //
    10  //     UROOT_QEMU='qemu-system-x86_64 -L . -m 4096 -enable-kvm'
    11  //
    12  // For CI, this environment variable is set in `.circleci/images/integration/Dockerfile`.
    13  package qemu
    14  
    15  import (
    16  	"fmt"
    17  	"io"
    18  	"net"
    19  	"os"
    20  	"regexp"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/google/goexpect"
    25  )
    26  
    27  // DefaultTimeout for `Expect` and `ExpectRE` functions.
    28  var DefaultTimeout = 7 * time.Second
    29  
    30  // TimeoutMultiplier increases all timeouts proportionally. Useful when running
    31  // QEMU on a slow machine.
    32  var TimeoutMultiplier = 2.0
    33  
    34  type Network struct {
    35  	port   uint16
    36  	numVMs uint8
    37  }
    38  
    39  func NewNetwork() *Network {
    40  	return &Network{
    41  		port: 1234,
    42  	}
    43  }
    44  
    45  func (n *Network) newVM() *networkState {
    46  	num := n.numVMs
    47  	n.numVMs++
    48  	return &networkState{
    49  		connect: num != 0,
    50  		mac:     net.HardwareAddr{0x0e, 0x00, 0x00, 0x00, 0x00, byte(num)},
    51  		port:    n.port,
    52  	}
    53  }
    54  
    55  type networkState struct {
    56  	// Whether to connect or listen.
    57  	connect bool
    58  	mac     net.HardwareAddr
    59  	port    uint16
    60  }
    61  
    62  // Options is filled and pass to `Start()`.
    63  type Options struct {
    64  	// Path to the bzImage kernel
    65  	Kernel string
    66  
    67  	// Path to the initramfs.
    68  	Initramfs string
    69  
    70  	// Extra kernel arguments.
    71  	KernelArgs string
    72  
    73  	// SharedDir is a directory that will be mountable inside the VM as
    74  	// /dev/sda1.
    75  	SharedDir string
    76  
    77  	// ExtraArgs are additional QEMU arguments.
    78  	ExtraArgs []string
    79  
    80  	// Where to send serial output.
    81  	SerialOutput io.WriteCloser
    82  
    83  	// Timeout is the expect timeout.
    84  	Timeout time.Duration
    85  
    86  	// Network to expose inside the VM.
    87  	Network *Network
    88  }
    89  
    90  // cmdline returns the command line arguments used to start QEMU. These
    91  // arguments are derived from the given QEMU struct.
    92  func cmdline(o *Options, net *networkState) []string {
    93  	// Read first few arguments for env.
    94  	env := os.Getenv("UROOT_QEMU")
    95  	if env == "" {
    96  		env = "qemu" // default
    97  	}
    98  	args := strings.Fields(env)
    99  
   100  	// Disable graphics because we are using serial.
   101  	args = append(args, "-nographic")
   102  
   103  	// Arguments passed to the kernel:
   104  	//
   105  	// - earlyprintk=ttyS0: print very early debug messages to the serial
   106  	// - console=ttyS0: /dev/console points to /dev/ttyS0 (the serial port)
   107  	// - o.KernelArgs: extra, optional kernel arguments
   108  	if len(o.Kernel) != 0 {
   109  		args = append(args, "-kernel", o.Kernel)
   110  		cmdline := "console=ttyS0 earlyprintk=ttyS0"
   111  		if len(o.KernelArgs) != 0 {
   112  			cmdline += " " + o.KernelArgs
   113  		}
   114  		args = append(args, "-append", cmdline)
   115  	}
   116  	if len(o.Initramfs) != 0 {
   117  		args = append(args, "-initrd", o.Initramfs)
   118  	}
   119  
   120  	if len(o.SharedDir) != 0 {
   121  		// Expose the temp directory to QEMU as /dev/sda1
   122  		args = append(args, "-drive", fmt.Sprintf("file=fat:ro:%s,if=none,id=tmpdir", o.SharedDir))
   123  		args = append(args, "-device", "ich9-ahci,id=ahci")
   124  		args = append(args, "-device", "ide-drive,drive=tmpdir,bus=ahci.0")
   125  	}
   126  
   127  	if net != nil {
   128  		args = append(args, "-net", fmt.Sprintf("nic,macaddr=%s", net.mac))
   129  		if net.connect {
   130  			args = append(args, "-net", fmt.Sprintf("socket,connect=:%d", net.port))
   131  		} else {
   132  			args = append(args, "-net", fmt.Sprintf("socket,listen=:%d", net.port))
   133  		}
   134  	}
   135  
   136  	if o.ExtraArgs != nil {
   137  		args = append(args, o.ExtraArgs...)
   138  	}
   139  	return args
   140  }
   141  
   142  // VM is a running QEMU virtual machine.
   143  type VM struct {
   144  	Options *Options
   145  	cmdline []string
   146  	network *networkState
   147  	gExpect *expect.GExpect
   148  }
   149  
   150  // Start a QEMU VM.
   151  func (o *Options) Start() (*VM, error) {
   152  	var net *networkState
   153  	if o.Network != nil {
   154  		net = o.Network.newVM()
   155  	}
   156  
   157  	cmdline := cmdline(o, net)
   158  
   159  	gExpect, _, err := expect.SpawnWithArgs(cmdline, -1,
   160  		expect.Tee(o.SerialOutput),
   161  		expect.CheckDuration(2*time.Millisecond))
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  	return &VM{
   166  		Options: o,
   167  		cmdline: cmdline,
   168  		network: net,
   169  		gExpect: gExpect,
   170  	}, nil
   171  }
   172  
   173  func (v *VM) Cmdline() []string {
   174  	// Maybe return a copy?
   175  	return v.cmdline
   176  }
   177  
   178  // CmdlineQuoted quotes any of QEMU's command line arguments containing a space
   179  // so it is easy to copy-n-paste into a shell for debugging.
   180  func (v *VM) CmdlineQuoted() string {
   181  	args := make([]string, len(v.cmdline))
   182  	for i, arg := range v.cmdline {
   183  		if strings.ContainsAny(arg, " \t\n") {
   184  			args[i] = fmt.Sprintf("'%s'", arg)
   185  		} else {
   186  			args[i] = arg
   187  		}
   188  	}
   189  	return strings.Join(args, " ")
   190  }
   191  
   192  // Close stops QEMU.
   193  func (v *VM) Close() {
   194  	v.gExpect.Close()
   195  	v.gExpect = nil
   196  }
   197  
   198  // Send sends a string to QEMU's serial.
   199  func (v *VM) Send(in string) {
   200  	v.gExpect.Send(in)
   201  }
   202  
   203  func (v *VM) TimeoutOr() time.Duration {
   204  	if v.Options.Timeout == 0 {
   205  		return DefaultTimeout
   206  	}
   207  	return v.Options.Timeout
   208  }
   209  
   210  // Expect returns an error if the given string is not found in vEMU's serial
   211  // output within `DefaultTimeout`.
   212  func (v *VM) Expect(search string) error {
   213  	return v.ExpectTimeout(search, v.TimeoutOr())
   214  }
   215  
   216  // ExpectTimeout returns an error if the given string is not found in vEMU's serial
   217  // output within the given timeout.
   218  func (v *VM) ExpectTimeout(search string, timeout time.Duration) error {
   219  	_, err := v.ExpectRETimeout(regexp.MustCompile(regexp.QuoteMeta(search)), timeout)
   220  	return err
   221  }
   222  
   223  // ExpectRE returns an error if the given regular expression is not found in
   224  // vEMU's serial output within `DefaultTimeout`. The matched string is
   225  // returned.
   226  func (v *VM) ExpectRE(pattern *regexp.Regexp) (string, error) {
   227  	return v.ExpectRETimeout(pattern, v.TimeoutOr())
   228  }
   229  
   230  // ExpectRETimeout returns an error if the given regular expression is not
   231  // found in vEMU's serial output within the given timeout. The matched string
   232  // is returned.
   233  func (v *VM) ExpectRETimeout(pattern *regexp.Regexp, timeout time.Duration) (string, error) {
   234  	scaled := time.Duration(float64(timeout) * TimeoutMultiplier)
   235  	str, _, err := v.gExpect.Expect(pattern, scaled)
   236  	return str, err
   237  }