blob: 556612ebd1ae36b54891d48474618a7507ba0130 [file] [log] [blame]
// Copyright 2017 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"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"os/signal"
"path"
"path/filepath"
"strings"
"syscall"
"time"
"fuchsia.googlesource.com/infra/infra/netboot"
"fuchsia.googlesource.com/infra/infra/pdu"
"fuchsia.googlesource.com/infra/infra/retry"
"fuchsia.googlesource.com/infra/infra/tftp"
)
const (
netbootFilenamePrefix = "<<netboot>>"
kernelFilename = netbootFilenamePrefix + "kernel.bin"
ramdiskFilename = netbootFilenamePrefix + "ramdisk.bin"
cmdlineFilename = netbootFilenamePrefix + "cmdline"
imageFilenamePrefix = "<<image>>"
efiFilename = imageFilenamePrefix + "efi.img"
kerncFilename = imageFilenamePrefix + "kernc.img"
fvmFilename = imageFilenamePrefix + "sparse.fvm"
summaryFilename = "summary.json"
)
type stringsFlag []string
func (s *stringsFlag) Set(val string) error {
*s = append(*s, val)
return nil
}
func (s *stringsFlag) String() string {
if s == nil {
return ""
}
return strings.Join([]string(*s), ", ")
}
var (
config string // config file path
kernelImage string // path to kernel image
ramdiskImage string // path to ramdisk image
efiImage string // path to efi image
kerncImage string // path to kernc image
fvmImages stringsFlag // list of paths to fvm images
testdir string // path to test directory
cmdline string // path to a file containing kernel cmdline args.
outfile string // path to output file
fileName string // name of the file in the test directory
filePollInterval time.Duration // poll interval for file on device
)
func init() {
flag.StringVar(&config, "config", "/etc/botanist/config.json", "config file path")
flag.StringVar(&kernelImage, "kernel", "", "path to kernel image")
flag.StringVar(&ramdiskImage, "ramdisk", "", "path to ramdisk image")
flag.StringVar(&efiImage, "efi", "", "path to efi image to be paved")
flag.StringVar(&kerncImage, "kernc", "", "path to kernc image to be paved")
flag.Var(&fvmImages, "fvm", "path to a sparse fvm image to be paved (may be specified up to 4 times)")
flag.StringVar(&cmdline, "cmdline", "", "path to a file containing command line arguments")
flag.StringVar(&testdir, "test", "/test", "path to test directory")
flag.StringVar(&outfile, "out", "output.tar", "path to output file")
flag.StringVar(&fileName, "file-name", summaryFilename, "name of the file in the test directory")
flag.DurationVar(&filePollInterval, "file-poll-interval", 1*time.Minute, "time between checking for summary.json on the target")
}
// Files is an ordered map for files which will be TFTP'd over to zedboot.
type Files struct {
remotes []string
locals [][]string
}
// Set adds a configuration of what files to send for to the remote name.
func (f *Files) Set(remote string, locals ...string) {
f.remotes = append(f.remotes, remote)
f.locals = append(f.locals, locals)
}
// Foreach calls a callback for each file to send in a filesMap.
func (f *Files) Foreach(c func(remote, local string) error) error {
for i := 0; i < len(f.remotes); i++ {
for j := 0; j < len(f.locals[i]); j++ {
if err := c(f.remotes[i], f.locals[i][j]); err != nil {
return err
}
}
}
return nil
}
// Config contains the application configuration.
type Config struct {
// Nodname is the hostname of the hardware device we want to boot on.
Nodename string `json:"nodename"`
// PDU is the configuration for the PDU attached to the hardware device.
PDU *pdu.Config `json:"pdu,omitempty"`
}
// Summary is the runtest summary output file format.
type Summary struct {
Tests []struct {
Name string `json:"name"`
OutputFile string `json:"output_file"`
Result string `json:"result"`
DataSinks map[string][]struct {
Name string `json:"name"`
File string `json:"file"`
} `json:"data_sinks,omitempty"`
} `json:"tests"`
Outputs map[string]string `json:"outputs,omitempty"`
}
func writeFileToTar(client *tftp.Client, tftpAddr *net.UDPAddr, tw *tar.Writer, testDir string, outputFile string) error {
writer, err := client.Receive(tftpAddr, path.Join(testDir, outputFile))
if err != nil {
return fmt.Errorf("failed to receive file %s: %v\n", outputFile, err)
}
hdr := &tar.Header{
Name: outputFile,
Size: writer.(tftp.Session).Size(),
Mode: 0666,
}
if err := tw.WriteHeader(hdr); err != nil {
return fmt.Errorf("failed to write file header: %v\n", err)
}
if _, err := writer.WriteTo(tw); err != nil {
return fmt.Errorf("failed to write file content: %v\n", err)
}
return nil
}
func runTests(ctx context.Context, config *Config, files *Files, args []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(config.Nodename, false)
return err
})
if err != nil {
return fmt.Errorf("cannot find node \"%s\": %v\n", config.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,
}
if err := transferFiles(ctx, client, tftpAddr, files, args); err != nil {
return fmt.Errorf("cannot transfer files: %v\n", err)
}
// Set up log listener and dump kernel output to stdout.
l, err := netboot.NewLogListener(config.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", fileName)
// 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(filePollInterval), func() error {
writer, err = client.Receive(tftpAddr, path.Join(testdir, fileName))
return err
})
if err != nil {
return fmt.Errorf("timed out waiting for tests to complete: %v", err)
}
log.Printf("reading \"%s\"\n", fileName)
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 Summary
if err := json.Unmarshal(buffer.Bytes(), &result); err != nil {
return fmt.Errorf("cannot unmarshall test results: %v\n", err)
}
file, err := os.OpenFile(outfile, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
return fmt.Errorf("failed to create file %s: %v\n", outfile, err)
}
tw := tar.NewWriter(file)
defer tw.Close()
hdr := &tar.Header{
Name: 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 _, path := range result.Outputs {
if err = writeFileToTar(client, tftpAddr, tw, testdir, path); err != nil {
return err
}
}
for _, test := range result.Tests {
if err = writeFileToTar(client, tftpAddr, tw, testdir, test.OutputFile); err != nil {
return err
}
// Copy data sinks if any are present.
for _, sinks := range test.DataSinks {
for _, sink := range sinks {
if err = writeFileToTar(client, tftpAddr, tw, testdir, sink.File); err != nil {
return err
}
}
}
}
return nil
}
// transferFiles sends kernel, optionally ramdisk, and any command line args to
// node.
func transferFiles(ctx context.Context, client *tftp.Client, addr *net.UDPAddr, files *Files, cmdline []string) error {
err := files.Foreach(func(remote, local string) error {
file, err := os.Open(local)
if err != nil {
return fmt.Errorf("cannot open %s: %v\n", local, err)
}
defer file.Close()
fi, err := file.Stat()
if err != nil {
return fmt.Errorf("cannot stat %s: %v\n", local, err)
}
// We need to retry because in the event of paving, TFTP might tell us
// it's not ready yet.
return retry.Retry(ctx, retry.WithMaxRetries(retry.NewConstantBackoff(time.Second), 60), func() error {
fmt.Printf("attempting to send %s=%s\n", remote, filepath.Base(local))
reader, err := client.Send(addr, remote, fi.Size())
if err != nil {
return fmt.Errorf("failed to send %s: %v\n", remote, err)
}
if _, err := reader.ReadFrom(file); err != nil {
return fmt.Errorf("failed to send %s data: %v\n", local, err)
}
return nil
})
})
if err != nil {
return err
}
if len(cmdline) > 0 {
var b bytes.Buffer
for _, arg := range cmdline {
fmt.Fprintf(&b, "%s\n", arg)
}
log.Printf("sending cmdline \"%s\"", b.String())
reader, err := client.Send(addr, cmdlineFilename, int64(b.Len()))
if err != nil {
return fmt.Errorf("failed to send cmdline: %v\n", err)
}
if _, err := reader.ReadFrom(bytes.NewReader(b.Bytes())); err != nil {
return fmt.Errorf("failed to read cmdline data: %v\n", err)
}
}
return nil
}
func loadConfig(ctx context.Context, path string) (*Config, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
var config Config
if err := json.NewDecoder(file).Decode(&config); err != nil {
return nil, err
}
return &config, err
}
func run(ctx context.Context, config *Config, files *Files, cmdline []string) error {
if config.PDU != nil {
defer func() {
log.Printf("rebooting the node \"%s\"\n", config.Nodename)
if err := pdu.RebootDevice(config.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() {
errs <- runTests(ctx, config, files, cmdline)
}()
select {
case err := <-errs:
return err
case <-signals:
cancel()
}
return nil
}
func mustExist(filename string) {
_, err := os.Stat(filename)
if err != nil {
log.Fatalf("failed to stat %s: %v", filename, err)
}
}
func main() {
flag.Parse()
ctx := context.Background()
config, err := loadConfig(ctx, config)
if err != nil {
log.Fatal(err)
}
// Files 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 Files
if ramdiskImage != "" {
mustExist(ramdiskImage)
files.Set(ramdiskFilename, ramdiskImage)
}
if len(fvmImages) != 0 {
for _, image := range fvmImages {
mustExist(image)
}
files.Set(fvmFilename, fvmImages...)
}
if efiImage != "" {
mustExist(efiImage)
files.Set(efiFilename, efiImage)
}
if kerncImage != "" {
mustExist(kerncImage)
files.Set(kerncFilename, kerncImage)
}
if kernelImage == "" {
log.Fatal("-kernel is required")
}
mustExist(kernelImage)
files.Set(kernelFilename, kernelImage)
// Aggregate all cmdline flags.
cmdlineArgs := flag.Args()
if cmdline != "" {
b, err := ioutil.ReadFile(cmdline)
if err != nil {
log.Fatal("failed to read cmdline file: %v", err)
}
cmdlineArgs = append(cmdlineArgs, strings.Split(string(b), "\n")...)
}
if err := run(ctx, config, &files, cmdlineArgs); err != nil {
log.Fatal(err)
}
}