github.com/coreos/mantle@v0.13.0/platform/qemu.go (about)

     1  // Copyright 2019 Red Hat
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package platform
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"io/ioutil"
    21  	"os"
    22  	"path/filepath"
    23  	"regexp"
    24  	"runtime"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/coreos/go-semver/semver"
    29  
    30  	"github.com/coreos/mantle/system/exec"
    31  	"github.com/coreos/mantle/util"
    32  )
    33  
    34  type MachineOptions struct {
    35  	AdditionalDisks []Disk
    36  }
    37  
    38  type Disk struct {
    39  	Size        string   // disk image size in bytes, optional suffixes "K", "M", "G", "T" allowed. Incompatible with BackingFile
    40  	BackingFile string   // raw disk image to use. Incompatible with Size.
    41  	DeviceOpts  []string // extra options to pass to qemu. "serial=XXXX" makes disks show up as /dev/disk/by-id/virtio-<serial>
    42  }
    43  
    44  var (
    45  	ErrNeedSizeOrFile  = errors.New("Disks need either Size or BackingFile specified")
    46  	ErrBothSizeAndFile = errors.New("Only one of Size and BackingFile can be specified")
    47  	primaryDiskOptions = []string{"serial=primary-disk"}
    48  )
    49  
    50  // Copy Container Linux input image and specialize copy for running kola tests.
    51  // Return FD to the copy, which is a deleted file.
    52  // This is not mandatory; the tests will do their best without it.
    53  func MakeCLDiskTemplate(inputPath string) (output *os.File, result error) {
    54  	seterr := func(err error) {
    55  		if result == nil {
    56  			result = err
    57  		}
    58  	}
    59  
    60  	// create output file
    61  	outputPath, err := mkpath("/var/tmp")
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	defer os.Remove(outputPath)
    66  
    67  	// copy file
    68  	// cp is used since it supports sparse and reflink.
    69  	cp := exec.Command("cp", "--force",
    70  		"--sparse=always", "--reflink=auto",
    71  		inputPath, outputPath)
    72  	cp.Stdout = os.Stdout
    73  	cp.Stderr = os.Stderr
    74  	if err := cp.Run(); err != nil {
    75  		return nil, fmt.Errorf("copying file: %v", err)
    76  	}
    77  
    78  	// create mount point
    79  	tmpdir, err := ioutil.TempDir("", "kola-qemu-")
    80  	if err != nil {
    81  		return nil, fmt.Errorf("making temporary directory: %v", err)
    82  	}
    83  	defer func() {
    84  		if err := os.Remove(tmpdir); err != nil {
    85  			seterr(fmt.Errorf("deleting directory %s: %v", tmpdir, err))
    86  		}
    87  	}()
    88  
    89  	// set up loop device
    90  	cmd := exec.Command("losetup", "-Pf", "--show", outputPath)
    91  	stdout, err := cmd.StdoutPipe()
    92  	if err != nil {
    93  		return nil, fmt.Errorf("getting stdout pipe: %v", err)
    94  	}
    95  	defer stdout.Close()
    96  	if err := cmd.Start(); err != nil {
    97  		return nil, fmt.Errorf("running losetup: %v", err)
    98  	}
    99  	buf, err := ioutil.ReadAll(stdout)
   100  	if err != nil {
   101  		cmd.Wait()
   102  		return nil, fmt.Errorf("reading losetup output: %v", err)
   103  	}
   104  	if err := cmd.Wait(); err != nil {
   105  		return nil, fmt.Errorf("setting up loop device: %v", err)
   106  	}
   107  	loopdev := strings.TrimSpace(string(buf))
   108  	defer func() {
   109  		if err := exec.Command("losetup", "-d", loopdev).Run(); err != nil {
   110  			seterr(fmt.Errorf("tearing down loop device: %v", err))
   111  		}
   112  	}()
   113  
   114  	// wait for OEM block device
   115  	oemdev := loopdev + "p6"
   116  	err = util.Retry(1000, 5*time.Millisecond, func() error {
   117  		if _, err := os.Stat(oemdev); !os.IsNotExist(err) {
   118  			return nil
   119  		}
   120  		return fmt.Errorf("timed out waiting for device node; did you specify a qcow image by mistake?")
   121  	})
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	// mount OEM partition
   127  	if err := exec.Command("mount", oemdev, tmpdir).Run(); err != nil {
   128  		return nil, fmt.Errorf("mounting OEM partition %s on %s: %v", oemdev, tmpdir, err)
   129  	}
   130  	defer func() {
   131  		if err := exec.Command("umount", tmpdir).Run(); err != nil {
   132  			seterr(fmt.Errorf("unmounting %s: %v", tmpdir, err))
   133  		}
   134  	}()
   135  
   136  	// write console settings to grub.cfg
   137  	f, err := os.OpenFile(filepath.Join(tmpdir, "grub.cfg"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
   138  	if err != nil {
   139  		return nil, fmt.Errorf("opening grub.cfg: %v", err)
   140  	}
   141  	defer f.Close()
   142  	if _, err = f.WriteString("set linux_console=\"console=ttyS0,115200\"\n"); err != nil {
   143  		return nil, fmt.Errorf("writing grub.cfg: %v", err)
   144  	}
   145  
   146  	// return fd to output file
   147  	output, err = os.Open(outputPath)
   148  	if err != nil {
   149  		return nil, fmt.Errorf("opening %v: %v", outputPath, err)
   150  	}
   151  	return
   152  }
   153  
   154  func (d Disk) getOpts() string {
   155  	if len(d.DeviceOpts) == 0 {
   156  		return ""
   157  	}
   158  	return "," + strings.Join(d.DeviceOpts, ",")
   159  }
   160  
   161  func (d Disk) setupFile() (*os.File, error) {
   162  	if d.Size == "" && d.BackingFile == "" {
   163  		return nil, ErrNeedSizeOrFile
   164  	}
   165  	if d.Size != "" && d.BackingFile != "" {
   166  		return nil, ErrBothSizeAndFile
   167  	}
   168  
   169  	if d.Size != "" {
   170  		return setupDisk(d.Size)
   171  	} else {
   172  		return setupDiskFromFile(d.BackingFile)
   173  	}
   174  }
   175  
   176  // Create a nameless temporary qcow2 image file backed by a raw image.
   177  func setupDiskFromFile(imageFile string) (*os.File, error) {
   178  	// a relative path would be interpreted relative to /tmp
   179  	backingFile, err := filepath.Abs(imageFile)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	// Keep the COW image from breaking if the "latest" symlink changes.
   184  	// Ignore /proc/*/fd/* paths, since they look like symlinks but
   185  	// really aren't.
   186  	if !strings.HasPrefix(backingFile, "/proc/") {
   187  		backingFile, err = filepath.EvalSymlinks(backingFile)
   188  		if err != nil {
   189  			return nil, err
   190  		}
   191  	}
   192  
   193  	qcowOpts := fmt.Sprintf("backing_file=%s,lazy_refcounts=on", backingFile)
   194  	return setupDisk("-o", qcowOpts)
   195  }
   196  
   197  func setupDisk(additionalOptions ...string) (*os.File, error) {
   198  	dstFileName, err := mkpath("")
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  	defer os.Remove(dstFileName)
   203  
   204  	opts := []string{"create", "-f", "qcow2", dstFileName}
   205  	opts = append(opts, additionalOptions...)
   206  
   207  	qemuImg := exec.Command("qemu-img", opts...)
   208  	qemuImg.Stderr = os.Stderr
   209  
   210  	if err := qemuImg.Run(); err != nil {
   211  		return nil, err
   212  	}
   213  
   214  	return os.OpenFile(dstFileName, os.O_RDWR, 0)
   215  }
   216  
   217  func mkpath(basedir string) (string, error) {
   218  	f, err := ioutil.TempFile(basedir, "mantle-qemu")
   219  	if err != nil {
   220  		return "", err
   221  	}
   222  	defer f.Close()
   223  	return f.Name(), nil
   224  }
   225  
   226  func CreateQEMUCommand(board, uuid, biosImage, consolePath, confPath, diskImagePath string, isIgnition bool, options MachineOptions) ([]string, []*os.File, error) {
   227  	var qmCmd []string
   228  
   229  	// As we expand this list of supported native + board
   230  	// archs combos we should coordinate with the
   231  	// coreos-assembler folks as they utilize something
   232  	// similar in cosa run
   233  	var qmBinary string
   234  	combo := runtime.GOARCH + "--" + board
   235  	switch combo {
   236  	case "amd64--amd64-usr":
   237  		qmBinary = "qemu-system-x86_64"
   238  		qmCmd = []string{
   239  			"qemu-system-x86_64",
   240  			"-machine", "accel=kvm",
   241  			"-cpu", "host",
   242  			"-m", "1024",
   243  		}
   244  	case "amd64--arm64-usr":
   245  		qmBinary = "qemu-system-aarch64"
   246  		qmCmd = []string{
   247  			"qemu-system-aarch64",
   248  			"-machine", "virt",
   249  			"-cpu", "cortex-a57",
   250  			"-m", "2048",
   251  		}
   252  	case "arm64--amd64-usr":
   253  		qmBinary = "qemu-system-x86_64"
   254  		qmCmd = []string{
   255  			"qemu-system-x86_64",
   256  			"-machine", "pc-q35-2.8",
   257  			"-cpu", "kvm64",
   258  			"-m", "1024",
   259  		}
   260  	case "arm64--arm64-usr":
   261  		qmBinary = "qemu-system-aarch64"
   262  		qmCmd = []string{
   263  			"qemu-system-aarch64",
   264  			"-machine", "virt,accel=kvm,gic-version=3",
   265  			"-cpu", "host",
   266  			"-m", "2048",
   267  		}
   268  	default:
   269  		panic("host-guest combo not supported: " + combo)
   270  	}
   271  
   272  	qmCmd = append(qmCmd,
   273  		"-bios", biosImage,
   274  		"-smp", "1",
   275  		"-uuid", uuid,
   276  		"-display", "none",
   277  		"-chardev", "file,id=log,path="+consolePath,
   278  		"-serial", "chardev:log",
   279  	)
   280  
   281  	if isIgnition {
   282  		qmCmd = append(qmCmd,
   283  			"-fw_cfg", "name=opt/com.coreos/config,file="+confPath)
   284  	} else {
   285  		qmCmd = append(qmCmd,
   286  			"-fsdev", "local,id=cfg,security_model=none,readonly,path="+confPath,
   287  			"-device", Virtio(board, "9p", "fsdev=cfg,mount_tag=config-2"))
   288  	}
   289  
   290  	// auto-read-only is only available in 3.1.0 & greater versions of QEMU
   291  	var autoReadOnly string
   292  	version, err := exec.Command(qmBinary, "--version").CombinedOutput()
   293  	if err != nil {
   294  		return nil, nil, fmt.Errorf("retrieving qemu version: %v", err)
   295  	}
   296  	pat := regexp.MustCompile(`version (\d+\.\d+\.\d+)`)
   297  	vNum := pat.FindSubmatch(version)
   298  	if len(vNum) < 2 {
   299  		return nil, nil, fmt.Errorf("unable to parse qemu version number")
   300  	}
   301  	qmSemver, err := semver.NewVersion(string(vNum[1]))
   302  	if err != nil {
   303  		return nil, nil, fmt.Errorf("parsing qemu semver: %v", err)
   304  	}
   305  	if !qmSemver.LessThan(*semver.New("3.1.0")) {
   306  		autoReadOnly = ",auto-read-only=off"
   307  		plog.Debugf("disabling auto-read-only for QEMU drives")
   308  	}
   309  
   310  	allDisks := append([]Disk{
   311  		{
   312  			BackingFile: diskImagePath,
   313  			DeviceOpts:  primaryDiskOptions,
   314  		},
   315  	}, options.AdditionalDisks...)
   316  
   317  	var extraFiles []*os.File
   318  	fdnum := 3 // first additional file starts at position 3
   319  	fdset := 1
   320  
   321  	for _, disk := range allDisks {
   322  		optionsDiskFile, err := disk.setupFile()
   323  		if err != nil {
   324  			return nil, nil, err
   325  		}
   326  		//defer optionsDiskFile.Close()
   327  		extraFiles = append(extraFiles, optionsDiskFile)
   328  
   329  		id := fmt.Sprintf("d%d", fdnum)
   330  		qmCmd = append(qmCmd, "-add-fd", fmt.Sprintf("fd=%d,set=%d", fdnum, fdset),
   331  			"-drive", fmt.Sprintf("if=none,id=%s,format=qcow2,file=/dev/fdset/%d%s", id, fdset, autoReadOnly),
   332  			"-device", Virtio(board, "blk", fmt.Sprintf("drive=%s%s", id, disk.getOpts())))
   333  		fdnum += 1
   334  		fdset += 1
   335  	}
   336  
   337  	return qmCmd, extraFiles, nil
   338  }
   339  
   340  // The virtio device name differs between machine types but otherwise
   341  // configuration is the same. Use this to help construct device args.
   342  func Virtio(board, device, args string) string {
   343  	var suffix string
   344  	switch board {
   345  	case "amd64-usr":
   346  		suffix = "pci"
   347  	case "arm64-usr":
   348  		suffix = "device"
   349  	default:
   350  		panic(board)
   351  	}
   352  	return fmt.Sprintf("virtio-%s-%s,%s", device, suffix, args)
   353  }