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 }