github.com/zaolin/u-root@v0.0.0-20200428085104-64aaafd46c6d/pkg/qemu/devices.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
     6  
     7  import (
     8  	"fmt"
     9  	"net"
    10  	"sync/atomic"
    11  )
    12  
    13  // Device is a QEMU device to expose to a VM.
    14  type Device interface {
    15  	// Cmdline returns arguments to append to the QEMU command line for this device.
    16  	Cmdline() []string
    17  	// KArgs returns arguments that must be passed to the kernel for this device, or nil.
    18  	KArgs() []string
    19  }
    20  
    21  // Network is a Device that can connect multiple QEMU VMs to each other.
    22  //
    23  // Network uses the QEMU socket mechanism to connect multiple VMs with a simple
    24  // TCP socket.
    25  type Network struct {
    26  	port uint16
    27  
    28  	// numVMs must be atomically accessed so VMs can be started in parallel
    29  	// in goroutines.
    30  	numVMs uint32
    31  }
    32  
    33  // NewNetwork creates a new QEMU network between QEMU VMs.
    34  //
    35  // The network is closed from the world and only between the QEMU VMs.
    36  func NewNetwork() *Network {
    37  	return &Network{
    38  		port: 1234,
    39  	}
    40  }
    41  
    42  // NetworkOpt returns additional QEMU command-line parameters based on the net
    43  // device ID.
    44  type NetworkOpt func(netdev string) []string
    45  
    46  // WithPCAP captures network traffic and saves it to outputFile.
    47  func WithPCAP(outputFile string) NetworkOpt {
    48  	return func(netdev string) []string {
    49  		return []string{
    50  			"-object",
    51  			// TODO(chrisko): generate an ID instead of 'f1'.
    52  			fmt.Sprintf("filter-dump,id=f1,netdev=%s,file=%s", netdev, outputFile),
    53  		}
    54  	}
    55  }
    56  
    57  // NewVM returns a Device that can be used with a new QEMU VM.
    58  func (n *Network) NewVM(nopts ...NetworkOpt) Device {
    59  	if n == nil {
    60  		return nil
    61  	}
    62  
    63  	newNum := atomic.AddUint32(&n.numVMs, 1)
    64  	num := newNum - 1
    65  
    66  	// MAC for the virtualized NIC.
    67  	//
    68  	// This is from the range of locally administered address ranges.
    69  	mac := net.HardwareAddr{0x0e, 0x00, 0x00, 0x00, 0x00, byte(num)}
    70  	devID := fmt.Sprintf("vm%d", num)
    71  
    72  	args := []string{"-device", fmt.Sprintf("e1000,netdev=%s,mac=%s", devID, mac)}
    73  	// Note: QEMU in CircleCI seems to in solve cases fail when using just ':1234' format.
    74  	// It fails with "address resolution failed for :1234: Name or service not known"
    75  	// hinting that this is somehow related to DNS resolution. To work around this,
    76  	// we explicitly bind to 127.0.0.1 (IPv6 [::1] is not parsed correctly by QEMU).
    77  	if num != 0 {
    78  		args = append(args, "-netdev", fmt.Sprintf("socket,id=%s,connect=127.0.0.1:%d", devID, n.port))
    79  	} else {
    80  		args = append(args, "-netdev", fmt.Sprintf("socket,id=%s,listen=127.0.0.1:%d", devID, n.port))
    81  	}
    82  
    83  	for _, opt := range nopts {
    84  		args = append(args, opt(devID)...)
    85  	}
    86  	return networkImpl{args}
    87  }
    88  
    89  type networkImpl struct {
    90  	args []string
    91  }
    92  
    93  func (n networkImpl) Cmdline() []string {
    94  	return n.args
    95  }
    96  
    97  func (n networkImpl) KArgs() []string { return nil }
    98  
    99  // ReadOnlyDirectory is a Device that exposes a directory as a /dev/sda1
   100  // readonly vfat partition in the VM.
   101  type ReadOnlyDirectory struct {
   102  	// Dir is the directory to expose as a read-only vfat partition.
   103  	Dir string
   104  }
   105  
   106  func (rod ReadOnlyDirectory) Cmdline() []string {
   107  	if len(rod.Dir) == 0 {
   108  		return nil
   109  	}
   110  
   111  	// Expose the temp directory to QEMU as /dev/sda1
   112  	return []string{
   113  		"-drive", fmt.Sprintf("file=fat:rw:%s,if=none,id=tmpdir", rod.Dir),
   114  		"-device", "ich9-ahci,id=ahci",
   115  		"-device", "ide-drive,drive=tmpdir,bus=ahci.0",
   116  	}
   117  }
   118  
   119  func (ReadOnlyDirectory) KArgs() []string { return nil }
   120  
   121  // P9Directory is a Device that exposes a directory as a Plan9 (9p)
   122  // read-write filesystem in the VM.
   123  type P9Directory struct {
   124  	// Dir is the directory to expose as read-write 9p filesystem.
   125  	Dir string
   126  
   127  	// Boot: if true, indicates this is the root volume. For this to work,
   128  	// kernel args will need to be added - use KArgs() to get the args. There
   129  	// can only be one boot 9pfs at a time.
   130  	Boot bool
   131  
   132  	// Tag is an identifier that is used within the VM when mounting an fs,
   133  	// e.g. 'mount -t 9p my-vol-ident mountpoint'. If not specified, a default
   134  	// tag of 'tmpdir' will be used.
   135  	//
   136  	// Ignored if Boot is true, as the tag in that case is special.
   137  	//
   138  	// For non-boot devices, this is also used as the id linking the `-fsdev`
   139  	// and `-device` args together.
   140  	//
   141  	// Because the tag must be unique for each dir, if multiple non-boot
   142  	// P9Directory's are used, tag may be omitted for no more than one.
   143  	Tag string
   144  
   145  	// Arch is the architecture under test. This is used to determine the
   146  	// QEMU command line args.
   147  	Arch string
   148  }
   149  
   150  func (p P9Directory) Cmdline() []string {
   151  	if len(p.Dir) == 0 {
   152  		return nil
   153  	}
   154  
   155  	var tag, id string
   156  	if p.Boot {
   157  		tag = "/dev/root"
   158  	} else {
   159  		tag = p.Tag
   160  		if len(tag) == 0 {
   161  			tag = "tmpdir"
   162  		}
   163  	}
   164  	if p.Boot {
   165  		id = "rootdrv"
   166  	} else {
   167  		id = tag
   168  	}
   169  
   170  	// Expose the temp directory to QEMU
   171  	var deviceArgs string
   172  	switch p.Arch {
   173  	case "arm":
   174  		deviceArgs = fmt.Sprintf("virtio-9p-device,fsdev=%s,mount_tag=%s", id, tag)
   175  	default:
   176  		deviceArgs = fmt.Sprintf("virtio-9p-pci,fsdev=%s,mount_tag=%s", id, tag)
   177  	}
   178  
   179  	return []string{
   180  		// security_model=mapped-file seems to be the best choice. It gives
   181  		// us control over uid/gid/mode seen in the guest, without requiring
   182  		// elevated perms on the host.
   183  		"-fsdev", fmt.Sprintf("local,id=%s,path=%s,security_model=mapped-file", id, p.Dir),
   184  		"-device", deviceArgs,
   185  	}
   186  }
   187  
   188  func (p P9Directory) KArgs() []string {
   189  	if len(p.Dir) == 0 {
   190  		return nil
   191  	}
   192  	if p.Boot {
   193  		return []string{
   194  			"devtmpfs.mount=1",
   195  			"root=/dev/root",
   196  			"rootfstype=9p",
   197  			"rootflags=trans=virtio,version=9p2000.L",
   198  		}
   199  	}
   200  	return []string{
   201  		//seen as an env var by the init process
   202  		"UROOT_USE_9P=1",
   203  	}
   204  }
   205  
   206  // VirtioRandom is a Device that exposes a PCI random number generator to the
   207  // QEMU VM.
   208  type VirtioRandom struct{}
   209  
   210  func (VirtioRandom) Cmdline() []string {
   211  	return []string{"-device", "virtio-rng-pci"}
   212  }
   213  
   214  func (VirtioRandom) KArgs() []string { return nil }
   215  
   216  // ArbitraryArgs is a Device that allows users to add arbitrary arguments to
   217  // the QEMU command line.
   218  type ArbitraryArgs []string
   219  
   220  func (aa ArbitraryArgs) Cmdline() []string {
   221  	return aa
   222  }
   223  
   224  func (ArbitraryArgs) KArgs() []string { return nil }