github.com/system-transparency/u-root@v6.0.1-0.20190919065413-ed07a650de4c+incompatible/pkg/vmtest/integration.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 vmtest
     6  
     7  import (
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"log"
    12  	"os"
    13  	"path"
    14  	"path/filepath"
    15  	"runtime"
    16  	"strings"
    17  	"testing"
    18  
    19  	"github.com/u-root/u-root/pkg/cp"
    20  	"github.com/u-root/u-root/pkg/golang"
    21  	"github.com/u-root/u-root/pkg/qemu"
    22  	"github.com/u-root/u-root/pkg/uio"
    23  	"github.com/u-root/u-root/pkg/uroot"
    24  	"github.com/u-root/u-root/pkg/uroot/initramfs"
    25  	"github.com/u-root/u-root/pkg/uroot/logger"
    26  )
    27  
    28  // Serial output is written to this directory and picked up by circleci, or
    29  // you, if you want to read the serial logs.
    30  const logDir = "serial"
    31  
    32  const template = `
    33  package main
    34  
    35  import (
    36  	"log"
    37  	"os"
    38  	"os/exec"
    39  )
    40  
    41  func main() {
    42  	for _, cmds := range %#v {
    43  		cmd := exec.Command(cmds[0], cmds[1:]...)
    44  		log.Printf("Execing %%#v", cmds)
    45  		cmd.Stdout = os.Stdout
    46  		cmd.Stderr = os.Stderr
    47  		err := cmd.Run()
    48  		if err != nil {
    49  			log.Fatal(err)
    50  		}
    51  	}
    52  }
    53  `
    54  
    55  // Options are integration test options.
    56  type Options struct {
    57  	// BuildOpts are u-root initramfs options.
    58  	//
    59  	// Fields that are not set are populated by QEMU and QEMUTest as
    60  	// possible.
    61  	BuildOpts uroot.Opts
    62  
    63  	// QEMUOpts are QEMU VM options for the test.
    64  	//
    65  	// Fields that are not set are populated by QEMU and QEMUTest as
    66  	// possible.
    67  	QEMUOpts qemu.Options
    68  
    69  	// DontSetEnv doesn't set the BuildOpts.Env and uses the user-supplied one.
    70  	//
    71  	// TODO: make uroot.Opts.Env a pointer?
    72  	DontSetEnv bool
    73  
    74  	// Name is the test's name.
    75  	//
    76  	// If name is left empty, the calling function's function name will be
    77  	// used as determined by runtime.Caller.
    78  	Name string
    79  
    80  	// Uinit are commands to execute after init.
    81  	//
    82  	// If populated, a uinit.go will be generated from these and added to
    83  	// the busybox generated in BuildOpts.Commands.
    84  	Uinit []string
    85  
    86  	// Logger logs build statements.
    87  	Logger logger.Logger
    88  
    89  	// Extra environment variables to set when building (used by u-bmc)
    90  	ExtraBuildEnv []string
    91  
    92  	// Use virtual vfat rather than 9pfs
    93  	UseVVFAT bool
    94  }
    95  
    96  func last(s string) string {
    97  	l := strings.Split(s, ".")
    98  	return l[len(l)-1]
    99  }
   100  
   101  type testLogger struct {
   102  	t *testing.T
   103  }
   104  
   105  func (tl testLogger) Printf(format string, v ...interface{}) {
   106  	tl.t.Logf(format, v...)
   107  }
   108  
   109  func (tl testLogger) Print(v ...interface{}) {
   110  	tl.t.Log(v...)
   111  }
   112  
   113  func callerName(depth int) string {
   114  	// Use the test name as the serial log's file name.
   115  	pc, _, _, ok := runtime.Caller(depth)
   116  	if !ok {
   117  		panic("runtime caller failed")
   118  	}
   119  	f := runtime.FuncForPC(pc)
   120  	return last(f.Name())
   121  }
   122  
   123  // TestLineWriter is an io.Writer that logs full lines of serial to tb.
   124  func TestLineWriter(tb testing.TB, prefix string) io.WriteCloser {
   125  	return uio.FullLineWriter(&testLineWriter{tb: tb, prefix: prefix})
   126  }
   127  
   128  type jsonStripper struct {
   129  	uio.LineWriter
   130  }
   131  
   132  func (j jsonStripper) OneLine(p []byte) {
   133  	// Poor man's JSON detector.
   134  	if len(p) == 0 || p[0] == '{' {
   135  		return
   136  	}
   137  	j.LineWriter.OneLine(p)
   138  }
   139  
   140  func JSONLessTestLineWriter(tb testing.TB, prefix string) io.WriteCloser {
   141  	return uio.FullLineWriter(jsonStripper{&testLineWriter{tb: tb, prefix: prefix}})
   142  }
   143  
   144  // testLineWriter is an io.Writer that logs full lines of serial to tb.
   145  type testLineWriter struct {
   146  	tb     testing.TB
   147  	prefix string
   148  }
   149  
   150  func (tsw *testLineWriter) OneLine(p []byte) {
   151  	tsw.tb.Logf("%s: %s", tsw.prefix, strings.ReplaceAll(string(p), "\033", "~"))
   152  }
   153  
   154  // TestArch returns the architecture under test. Pass this as GOARCH when
   155  // building Go programs to be run in the QEMU environment.
   156  func TestArch() string {
   157  	if env := os.Getenv("UROOT_TESTARCH"); env != "" {
   158  		return env
   159  	}
   160  	return "amd64"
   161  }
   162  
   163  // SkipWithoutQEMU skips the test when the QEMU environment variables are not
   164  // set. This is already called by QEMUTest(), so use if some expensive
   165  // operations are performed before calling QEMUTest().
   166  func SkipWithoutQEMU(t *testing.T) {
   167  	if _, ok := os.LookupEnv("UROOT_QEMU"); !ok {
   168  		t.Skip("QEMU test is skipped unless UROOT_QEMU is set")
   169  	}
   170  	if _, ok := os.LookupEnv("UROOT_KERNEL"); !ok {
   171  		t.Skip("QEMU test is skipped unless UROOT_KERNEL is set")
   172  	}
   173  }
   174  
   175  func QEMUTest(t *testing.T, o *Options) (*qemu.VM, func()) {
   176  	SkipWithoutQEMU(t)
   177  
   178  	if len(o.Name) == 0 {
   179  		o.Name = callerName(2)
   180  	}
   181  	if o.Logger == nil {
   182  		o.Logger = &testLogger{t}
   183  	}
   184  	if o.QEMUOpts.SerialOutput == nil {
   185  		o.QEMUOpts.SerialOutput = TestLineWriter(t, "serial")
   186  	}
   187  	if TestArch() == "arm" {
   188  		//currently, 9p does not work on arm
   189  		o.UseVVFAT = true
   190  	}
   191  
   192  	qOpts, tmpDir, err := QEMU(o)
   193  	if err != nil {
   194  		t.Fatalf("Failed to create QEMU VM %s: %v", o.Name, err)
   195  	}
   196  
   197  	vm, err := qOpts.Start()
   198  	if err != nil {
   199  		t.Fatalf("Failed to start QEMU VM %s: %v", o.Name, err)
   200  	}
   201  	t.Logf("QEMU command line for %s:\n%s", o.Name, vm.CmdlineQuoted())
   202  
   203  	return vm, func() {
   204  		vm.Close()
   205  		if t.Failed() {
   206  			t.Log("Keeping temp dir: ", tmpDir)
   207  		} else if len(o.BuildOpts.TempDir) == 0 {
   208  			if err := os.RemoveAll(o.BuildOpts.TempDir); err != nil {
   209  				t.Logf("failed to remove temporary directory %s: %v", o.BuildOpts.TempDir, err)
   210  			}
   211  		}
   212  	}
   213  }
   214  
   215  // QEMU builds the u-root environment and prepares QEMU options given the test
   216  // options and environment variables.
   217  //
   218  // QEMU will augment o.BuildOpts and o.QEMUOpts with configuration that the
   219  // caller either requested (through the Options.Uinit field, for example) or
   220  // that the caller did not set.
   221  //
   222  // QEMU returns the QEMU launch options, the temporary directory exposed to the
   223  // QEMU VM, or an error.
   224  func QEMU(o *Options) (*qemu.Options, string, error) {
   225  	if len(o.Name) == 0 {
   226  		o.Name = callerName(2)
   227  	}
   228  
   229  	if len(o.QEMUOpts.Initramfs) == 0 {
   230  		if !o.DontSetEnv {
   231  			env := golang.Default()
   232  			env.CgoEnabled = false
   233  			env.GOARCH = TestArch()
   234  			o.BuildOpts.Env = env
   235  		}
   236  
   237  		var cmds []string
   238  		if len(o.BuildOpts.Commands) == 0 {
   239  			cmds = append(cmds, "github.com/u-root/u-root/cmds/*")
   240  		}
   241  		// Create a uinit from the commands given.
   242  		if len(o.Uinit) > 0 {
   243  			urootPkg, err := o.BuildOpts.Env.Package("github.com/u-root/u-root/integration")
   244  			if err != nil {
   245  				return nil, "", err
   246  			}
   247  			testDir := filepath.Join(urootPkg.Dir, "testcmd")
   248  
   249  			dirpath, err := ioutil.TempDir(testDir, "uinit-")
   250  			if err != nil {
   251  				return nil, "", err
   252  			}
   253  			defer os.RemoveAll(dirpath)
   254  
   255  			if err := os.MkdirAll(filepath.Join(dirpath, "uinit"), 0755); err != nil {
   256  				return nil, "", err
   257  			}
   258  
   259  			var realUinit [][]string
   260  			for _, cmd := range o.Uinit {
   261  				realUinit = append(realUinit, fields(cmd))
   262  			}
   263  
   264  			if err := ioutil.WriteFile(
   265  				filepath.Join(dirpath, "uinit", "uinit.go"),
   266  				[]byte(fmt.Sprintf(template, realUinit)),
   267  				0755); err != nil {
   268  				return nil, "", err
   269  			}
   270  			cmds = append(cmds, path.Join("github.com/u-root/u-root/integration/testcmd", filepath.Base(dirpath), "uinit"))
   271  		}
   272  		// Add our commands to the build opts.
   273  		if len(cmds) > 0 {
   274  			o.BuildOpts.AddBusyBoxCommands(cmds...)
   275  		}
   276  
   277  		// Create or reuse a temporary directory.
   278  		if len(o.BuildOpts.TempDir) == 0 {
   279  			tmpDir, err := ioutil.TempDir("", "uroot-integration")
   280  			if err != nil {
   281  				return nil, "", err
   282  			}
   283  			o.BuildOpts.TempDir = tmpDir
   284  		}
   285  		if o.BuildOpts.BaseArchive == nil {
   286  			o.BuildOpts.BaseArchive = uroot.DefaultRamfs.Reader()
   287  		}
   288  		if len(o.BuildOpts.InitCmd) == 0 {
   289  			o.BuildOpts.InitCmd = "init"
   290  		}
   291  		if len(o.BuildOpts.DefaultShell) == 0 {
   292  			o.BuildOpts.DefaultShell = "elvish"
   293  		}
   294  
   295  		if o.Logger == nil {
   296  			o.Logger = log.New(os.Stderr, "", 0)
   297  		}
   298  
   299  		// OutputFile
   300  		var outputFile string
   301  		if o.BuildOpts.OutputFile == nil {
   302  			outputFile = filepath.Join(o.BuildOpts.TempDir, "initramfs.cpio")
   303  			w, err := initramfs.CPIO.OpenWriter(o.Logger, outputFile, "", "")
   304  			if err != nil {
   305  				return nil, "", err
   306  			}
   307  			o.BuildOpts.OutputFile = w
   308  		}
   309  
   310  		// Finally, create an initramfs!
   311  		if err := uroot.CreateInitramfs(o.Logger, o.BuildOpts); err != nil {
   312  			return nil, "", err
   313  		}
   314  
   315  		o.QEMUOpts.Initramfs = outputFile
   316  	}
   317  
   318  	if len(o.QEMUOpts.Kernel) == 0 {
   319  		// Copy kernel to tmpDir for tests involving kexec.
   320  		kernel := filepath.Join(o.BuildOpts.TempDir, "kernel")
   321  		if err := cp.Copy(os.Getenv("UROOT_KERNEL"), kernel); err != nil {
   322  			return nil, "", err
   323  		}
   324  		o.QEMUOpts.Kernel = kernel
   325  	}
   326  
   327  	switch TestArch() {
   328  	case "amd64":
   329  		o.QEMUOpts.KernelArgs += " console=ttyS0 earlyprintk=ttyS0"
   330  	case "arm":
   331  		o.QEMUOpts.KernelArgs += " console=ttyAMA0"
   332  	}
   333  
   334  	var dir qemu.Device
   335  	if o.UseVVFAT {
   336  		dir = qemu.ReadOnlyDirectory{Dir: o.BuildOpts.TempDir}
   337  	} else {
   338  		dir = qemu.P9Directory{Dir: o.BuildOpts.TempDir}
   339  	}
   340  	o.QEMUOpts.Devices = append(o.QEMUOpts.Devices, qemu.VirtioRandom{}, dir)
   341  
   342  	return &o.QEMUOpts, o.BuildOpts.TempDir, nil
   343  }