blob: f444628b74924c341c1d294deef968149f77f5c9 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"archive/tar"
"bytes"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"os/signal"
"path"
"strings"
"syscall"
"time"
"fuchsia.googlesource.com/infra/infra/botanist"
"fuchsia.googlesource.com/infra/infra/fastboot"
"fuchsia.googlesource.com/infra/infra/netboot"
"fuchsia.googlesource.com/infra/infra/pdu"
"fuchsia.googlesource.com/infra/infra/retry"
"fuchsia.googlesource.com/infra/infra/tftp"
"github.com/google/subcommands"
)
// ZedbootCommand is a Command implementation for running the testing workflow on a device
// that boots with Zedboot.
type ZedbootCommand struct {
// KernelImage is the path to a kernel image.
kernelImage string
// RamdiskImage is the path to a ramdisk image.
ramdiskImage string
// EfiImage is the path to an EFI image.
efiImage string
// KerncImage is the path to a kernc image.
kerncImage string
// FVMImages is a list of paths to sparse fvm images to be paved.
fvmImages botanist.StringsFlag
// ZedbootImage is the path to the zedboot image.
zedbootImage string
// PropertiesFile is the path to a file where deviceProperties have been written.
propertiesFile string
// TestResultsDir is the directory on target to where test results will be written.
testResultsDir string
// SummaryFilename is the name of the test summary JSON file to be written to
// testResultsDir.
summaryFilename string
// FilePollInterval is the duration waited between checking for test summary file
// on the target to be written.
filePollInterval time.Duration
// OutputArchive is a path on host to where the tarball containing the test results
// will be output.
outputArchive string
// CmdlineFile is the path to a file of additional kernel command-line arguments.
cmdlineFile string
// FastbootTool is the path to the fastboot tool.
fastbootTool string
// Fastboot is true iff we will use first use fastboot to flash Zedboot and boot into Zedboot.
fastboot bool
}
func (*ZedbootCommand) Name() string {
return "zedboot"
}
func (*ZedbootCommand) Usage() string {
return "zedboot [flags...] [kernel command-line arguments...]\n\nflags:\n"
}
func (*ZedbootCommand) Synopsis() string {
return "boots a Zedboot device and collects test results"
}
func (cmd *ZedbootCommand) SetFlags(f *flag.FlagSet) {
f.StringVar(&cmd.kernelImage, "kernel", "", "path to kernel image")
f.StringVar(&cmd.ramdiskImage, "ramdisk", "", "path to ramdisk image")
f.StringVar(&cmd.efiImage, "efi", "", "path to EFI image to be paved")
f.StringVar(&cmd.kerncImage, "kernc", "", "path to kernc image to be paved")
f.Var(&cmd.fvmImages, "fvm", "paths to sparse FVM images to be paved (may be specified up to 4 times)")
f.StringVar(&cmd.zedbootImage, "zedboot", "", "path to zedboot image to be flashed. Must be set together with -fastboot")
f.StringVar(&cmd.testResultsDir, "results-dir", "/test", "path on target to where test results will be written")
f.StringVar(&cmd.outputArchive, "out", "output.tar", "path on host to output tarball of test results")
f.StringVar(&cmd.summaryFilename, "summary-name", botanist.TestSummaryFilename, "name of the file in the test directory")
f.DurationVar(&cmd.filePollInterval, "poll-interval", 1*time.Minute, "time between checking for summary.json on the target")
f.StringVar(&cmd.propertiesFile, "properties", "/etc/botanist/config.json", "path to file of device properties")
f.StringVar(&cmd.cmdlineFile, "cmdline-file", "", "path to a file containing additional kernel command-line arguments")
f.StringVar(&cmd.fastbootTool, "fastboot-tool", "./fastboot/fastboot", "path to the fastboot tool.")
f.BoolVar(&cmd.fastboot, "fastboot", false, "If set, -fastboot-tool will be used to put the device into zedboot before "+
"doing anything else. Must be set together with -zedboot")
}
func (cmd *ZedbootCommand) validateImages() error {
var errs []string
existsIfSet := func(filename string) {
if filename == "" {
return
}
_, err := os.Stat(filename)
if err != nil {
errs = append(errs, fmt.Sprintf("failed to stat %s: %v", filename, err.Error()))
}
}
if cmd.kernelImage == "" {
errs = append(errs, "|kernelImage| must be set.")
}
existsIfSet(cmd.kernelImage)
existsIfSet(cmd.ramdiskImage)
existsIfSet(cmd.efiImage)
existsIfSet(cmd.kerncImage)
for _, image := range cmd.fvmImages {
existsIfSet(image)
}
if cmd.fastboot {
existsIfSet(cmd.zedbootImage)
existsIfSet(cmd.fastbootTool)
}
if len(errs) > 0 {
return errors.New(strings.Join(errs, "\n"))
}
return nil
}
func (cmd *ZedbootCommand) runTests(ctx context.Context, nodename string, cmdlineArgs []string) error {
// Find the node address UDP address.
n := netboot.NewClient(time.Second)
var addr *net.UDPAddr
var err error
// We need to retry here because botanist might try to discover before
// zedboot is fully ready, so the packet that's sent out doesn't result
// in any reply. We don't need to wait between calls because Discover
// already has a 1 minute timeout for reading a UDP packet from zedboot.
err = retry.Retry(ctx, retry.WithMaxRetries(retry.NewConstantBackoff(time.Second), 60), func() error {
addr, err = n.Discover(nodename, false)
return err
})
if err != nil {
return fmt.Errorf("cannot find node \"%s\": %v\n", nodename, err)
}
// Transfer kernel, ramdisk, and command line args onto the node.
client := tftp.NewClient()
tftpAddr := &net.UDPAddr{
IP: addr.IP,
Port: tftp.ClientPort,
Zone: addr.Zone,
}
// TFTPFiles is effectively an ordered map, so the order in which
// its contents are added is the order in which they'll be transferred
// later.
var files botanist.TFTPFiles
if cmd.ramdiskImage != "" {
files.Set(botanist.RamdiskFilename, cmd.ramdiskImage)
}
if cmd.fvmImages != nil {
files.Set(botanist.FVMFilename, cmd.fvmImages...)
}
if cmd.efiImage != "" {
files.Set(botanist.EFIFilename, cmd.efiImage)
}
if cmd.kerncImage != "" {
files.Set(botanist.KerncFilename, cmd.kerncImage)
}
if cmd.kernelImage != "" {
files.Set(botanist.KernelFilename, cmd.kernelImage)
}
if err := files.Transfer(ctx, client, tftpAddr); err != nil {
return fmt.Errorf("cannot transfer files: %v\n", err)
}
if err := botanist.TransferCmdlineArgs(client, tftpAddr, cmdlineArgs); err != nil {
return fmt.Errorf("cannot transer command-line arguments: %v\n", cmdlineArgs)
}
// Set up log listener and dump kernel output to stdout.
l, err := netboot.NewLogListener(nodename)
if err != nil {
return fmt.Errorf("cannot listen: %v\n", err)
}
go func() {
defer l.Close()
log.Printf("starting log listener\n")
for {
data, err := l.Listen()
if err != nil {
continue
}
fmt.Print(data)
select {
case <-ctx.Done():
return
default:
}
}
}()
log.Printf("sending boot command\n")
// Boot Fuchsia.
if err := n.Boot(addr); err != nil {
return fmt.Errorf("cannot boot: %v\n", err)
}
log.Printf("waiting for \"%s\"\n", cmd.summaryFilename)
// Poll for summary.json; this relies on runtest being executed using
// autorun and it eventually producing the summary.json file.
var buffer bytes.Buffer
var writer io.WriterTo
err = retry.Retry(ctx, retry.NewConstantBackoff(cmd.filePollInterval), func() error {
writer, err = client.Receive(tftpAddr, path.Join(cmd.testResultsDir, cmd.summaryFilename))
return err
})
if err != nil {
return fmt.Errorf("timed out waiting for tests to complete: %v", err)
}
log.Printf("reading \"%s\"\n", cmd.summaryFilename)
if _, err := writer.WriteTo(&buffer); err != nil {
return fmt.Errorf("failed to receive summary file: %v\n", err)
}
// Parse and save the summary.json file.
var result botanist.TestSummary
if err := json.Unmarshal(buffer.Bytes(), &result); err != nil {
return fmt.Errorf("cannot unmarshall test results: %v\n", err)
}
file, err := os.OpenFile(cmd.outputArchive, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
return fmt.Errorf("failed to create file %s: %v\n", cmd.outputArchive, err)
}
tw := tar.NewWriter(file)
defer tw.Close()
hdr := &tar.Header{
Name: cmd.summaryFilename,
Size: int64(buffer.Len()),
Mode: 0666,
}
if err := tw.WriteHeader(hdr); err != nil {
return fmt.Errorf("failed to write summary header: %v\n", err)
}
if _, err := tw.Write(buffer.Bytes()); err != nil {
return fmt.Errorf("failed to write summary content: %v\n", err)
log.Fatal(err)
}
log.Printf("copying test output\n")
// Copy test output from the node.
for _, output := range result.Outputs {
if err = botanist.WriteFileToTar(client, tftpAddr, tw, cmd.testResultsDir, output); err != nil {
return err
}
}
for _, test := range result.Tests {
if err = botanist.WriteFileToTar(client, tftpAddr, tw, cmd.testResultsDir, test.OutputFile); err != nil {
return err
}
// Copy data sinks if any are present.
for _, sinks := range test.DataSinks {
for _, sink := range sinks {
if err = botanist.WriteFileToTar(client, tftpAddr, tw, cmd.testResultsDir, sink.File); err != nil {
return err
}
}
}
}
return nil
}
func (cmd *ZedbootCommand) fastbootToZedboot(ctx context.Context) error {
// If it can't find any fastboot device, the fastboot tool will hang waiting, so we add a timeout.
// All fastboot operations take less than a second on a developer workstation, so a
// minute per each operation is very generous.
ctx, _ = context.WithTimeout(ctx, 2*time.Minute)
f := fastboot.Fastboot{cmd.fastbootTool}
log.Printf("fastboot flashing zedboot image")
if _, err := f.Flash(ctx, "boot", cmd.zedbootImage); err != nil {
return fmt.Errorf("failed to flash the fastboot device: %v", err)
}
log.Printf("continuing from fastboot into zedboot")
if _, err := f.Continue(ctx); err != nil {
return fmt.Errorf("failed to boot the device with \"fastboot continue\": %v", err)
}
return nil
}
func (cmd *ZedbootCommand) execute(ctx context.Context, cmdlineArgs []string) error {
var properties botanist.DeviceProperties
if err := botanist.LoadDeviceProperties(cmd.propertiesFile, &properties); err != nil {
return fmt.Errorf("failed to open device properties file \"%v\"", cmd.propertiesFile)
}
if err := cmd.validateImages(); err != nil {
return err
}
if properties.PDU != nil {
defer func() {
log.Printf("rebooting the node \"%s\"\n", properties.Nodename)
if err := pdu.RebootDevice(properties.PDU); err != nil {
log.Fatalf("failed to reboot the device: %v", err)
}
}()
}
ctx, cancel := context.WithCancel(ctx)
// Handle SIGTERM and make sure we send a reboot to the device.
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGTERM)
errs := make(chan error)
go func() {
if cmd.fastboot {
if err := cmd.fastbootToZedboot(ctx); err != nil {
errs <- err
return
}
}
errs <- cmd.runTests(ctx, properties.Nodename, cmdlineArgs)
}()
select {
case err := <-errs:
return err
case <-signals:
cancel()
}
return nil
}
func (cmd *ZedbootCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
flag := f.Lookup("properties")
log.Printf("properties flag: %v", flag)
if cmd.fastboot {
if cmd.zedbootImage == "" {
log.Print("-fastboot is set but -zedboot is not")
return subcommands.ExitFailure
}
if cmd.fastbootTool == "" {
log.Print("-fastboot is set but -fastboot-tool is empty")
return subcommands.ExitFailure
}
}
// Aggregate command-line arguments.
cmdlineArgs := f.Args()
if cmd.cmdlineFile != "" {
args, err := ioutil.ReadFile(cmd.cmdlineFile)
if err != nil {
log.Printf("failed to read command-line args file \"%v\": %v", cmd.cmdlineFile, err)
return subcommands.ExitFailure
}
cmdlineArgs = append(cmdlineArgs, strings.Split(string(args), "\n")...)
}
if err := cmd.execute(ctx, cmdlineArgs); err != nil {
log.Print(err)
return subcommands.ExitFailure
}
return subcommands.ExitSuccess
}