github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/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-hd,drive=tmpdir,bus=ahci.0",
   116  	}
   117  }
   118  
   119  func (ReadOnlyDirectory) KArgs() []string { return nil }
   120  
   121  // IDEBlockDevice emulates an AHCI/IDE block device.
   122  type IDEBlockDevice struct {
   123  	File string
   124  }
   125  
   126  func (ibd IDEBlockDevice) Cmdline() []string {
   127  	if len(ibd.File) == 0 {
   128  		return nil
   129  	}
   130  
   131  	// There's a better way to do this. I don't know what it is. Some day
   132  	// I'll learn QEMU's crazy command line.
   133  	//
   134  	// I wish someone would make a proto representation of the
   135  	// command-line. Would be so much more understandable how device
   136  	// backends and frontends relate to each other.
   137  	return []string{
   138  		"-device", "ich9-ahci,id=ahci",
   139  		"-device", "ide-hd,drive=disk,bus=ahci.0",
   140  		"-drive", fmt.Sprintf("file=%s,if=none,id=disk", ibd.File),
   141  	}
   142  }
   143  
   144  func (IDEBlockDevice) KArgs() []string { return nil }
   145  
   146  // P9Directory is a Device that exposes a directory as a Plan9 (9p)
   147  // read-write filesystem in the VM.
   148  type P9Directory struct {
   149  	// Dir is the directory to expose as read-write 9p filesystem.
   150  	Dir string
   151  
   152  	// Boot: if true, indicates this is the root volume. For this to work,
   153  	// kernel args will need to be added - use KArgs() to get the args. There
   154  	// can only be one boot 9pfs at a time.
   155  	Boot bool
   156  
   157  	// Tag is an identifier that is used within the VM when mounting an fs,
   158  	// e.g. 'mount -t 9p my-vol-ident mountpoint'. If not specified, a default
   159  	// tag of 'tmpdir' will be used.
   160  	//
   161  	// Ignored if Boot is true, as the tag in that case is special.
   162  	//
   163  	// For non-boot devices, this is also used as the id linking the `-fsdev`
   164  	// and `-device` args together.
   165  	//
   166  	// Because the tag must be unique for each dir, if multiple non-boot
   167  	// P9Directory's are used, tag may be omitted for no more than one.
   168  	Tag string
   169  
   170  	// Arch is the architecture under test. This is used to determine the
   171  	// QEMU command line args.
   172  	Arch string
   173  }
   174  
   175  func (p P9Directory) Cmdline() []string {
   176  	if len(p.Dir) == 0 {
   177  		return nil
   178  	}
   179  
   180  	var tag, id string
   181  	if p.Boot {
   182  		tag = "/dev/root"
   183  	} else {
   184  		tag = p.Tag
   185  		if len(tag) == 0 {
   186  			tag = "tmpdir"
   187  		}
   188  	}
   189  	if p.Boot {
   190  		id = "rootdrv"
   191  	} else {
   192  		id = tag
   193  	}
   194  
   195  	// Expose the temp directory to QEMU
   196  	var deviceArgs string
   197  	switch p.Arch {
   198  	case "arm":
   199  		deviceArgs = fmt.Sprintf("virtio-9p-device,fsdev=%s,mount_tag=%s", id, tag)
   200  	default:
   201  		deviceArgs = fmt.Sprintf("virtio-9p-pci,fsdev=%s,mount_tag=%s", id, tag)
   202  	}
   203  
   204  	return []string{
   205  		// security_model=mapped-file seems to be the best choice. It gives
   206  		// us control over uid/gid/mode seen in the guest, without requiring
   207  		// elevated perms on the host.
   208  		"-fsdev", fmt.Sprintf("local,id=%s,path=%s,security_model=mapped-file", id, p.Dir),
   209  		"-device", deviceArgs,
   210  	}
   211  }
   212  
   213  func (p P9Directory) KArgs() []string {
   214  	if len(p.Dir) == 0 {
   215  		return nil
   216  	}
   217  	if p.Boot {
   218  		return []string{
   219  			"devtmpfs.mount=1",
   220  			"root=/dev/root",
   221  			"rootfstype=9p",
   222  			"rootflags=trans=virtio,version=9p2000.L",
   223  		}
   224  	}
   225  	return []string{
   226  		// seen as an env var by the init process
   227  		"UROOT_USE_9P=1",
   228  	}
   229  }
   230  
   231  // VirtioRandom is a Device that exposes a PCI random number generator to the
   232  // QEMU VM.
   233  type VirtioRandom struct{}
   234  
   235  func (VirtioRandom) Cmdline() []string {
   236  	return []string{"-device", "virtio-rng-pci"}
   237  }
   238  
   239  func (VirtioRandom) KArgs() []string { return nil }
   240  
   241  // ArbitraryArgs is a Device that allows users to add arbitrary arguments to
   242  // the QEMU command line.
   243  type ArbitraryArgs []string
   244  
   245  func (aa ArbitraryArgs) Cmdline() []string {
   246  	return aa
   247  }
   248  
   249  func (ArbitraryArgs) KArgs() []string { return nil }