blob: c034321709a3694c2510c142a6db65308988a8c2 [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"
"log"
"net"
"net/http"
"os"
"os/signal"
"path"
"strconv"
"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 (
filenamePrefix = "<<netboot>>"
kernelFilename = filenamePrefix + "kernel.bin"
ramdiskFilename = filenamePrefix + "ramdisk.bin"
cmdlineFilename = filenamePrefix + "cmdline"
summaryFilename = "summary.json"
)
var (
config string // config file path
kernel string // path to kernel image
ramdisk string // path to ramdisk image
testdir string // path to test directory
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
timeout time.Duration // global timeout
)
func init() {
flag.StringVar(&config, "config", "/etc/botanist/config.json", "config file path")
flag.StringVar(&kernel, "kernel", "", "path to kernel image")
flag.StringVar(&ramdisk, "ramdisk", "", "path to ramdisk image")
flag.StringVar(&testdir, "test", "/test", "path to test directory")
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")
flag.StringVar(&outfile, "out", "output.tar", "path to output file")
flag.DurationVar(&timeout, "timeout", 1*time.Minute, "timeout")
}
// 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 *struct {
// Host is the network hostname of the PDU e.g. fuchsia-tests-pdu-001.
Host string `json:"host"`
// Username is the username used to log in to the PDU.
Username string `json:"username"`
// Password is the password used to log in to the PDU.
Password string `json:"password"`
// DevicePort is the PDU-specific string which identifies the
// hardware device we're testing on in the PDU.
DevicePort string `json:"device_port"`
} `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"`
} `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, kernel, ramdisk string, 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(client, tftpAddr, kernel, ramdisk, 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
}
}
return nil
}
// transferFiles sends kernel, optionally ramdisk, and any command line args to
// node.
func transferFiles(client *tftp.Client, addr *net.UDPAddr, kernel string, ramdisk string, cmdline []string) error {
if ramdisk != "" {
file, err := os.Open(ramdisk)
if err != nil {
return fmt.Errorf("cannot open ramdisk: %v\n", err)
}
defer file.Close()
fi, err := file.Stat()
if err != nil {
return fmt.Errorf("cannot stat ramdisk: %v\n", err)
}
log.Printf("sending ramdisk \"%s\"", ramdisk)
reader, err := client.Send(addr, ramdiskFilename, fi.Size())
if err != nil {
return fmt.Errorf("failed to send ramdisk: %v\n", err)
}
if _, err := reader.ReadFrom(file); err != nil {
return fmt.Errorf("failed to send ramdisk data: %v\n", err)
}
}
file, err := os.Open(kernel)
if err != nil {
return fmt.Errorf("cannot open kernel: %v\n", err)
}
defer file.Close()
fi, err := file.Stat()
if err != nil {
return fmt.Errorf("cannot stat kernel: %v\n", err)
}
log.Printf("sending kernel \"%s\"", kernel)
reader, err := client.Send(addr, kernelFilename, fi.Size())
if err != nil {
return fmt.Errorf("failed to send kernel: %v\n", err)
}
if _, err := reader.ReadFrom(file); err != nil {
return fmt.Errorf("failed to send kernel data: %v\n", 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 rebootDevice(ctx context.Context, config *Config) error {
wc := &pdu.Webcardlx{
Client: &http.Client{},
Host: config.PDU.Host,
Username: config.PDU.Username,
Password: config.PDU.Password,
}
if err := wc.Login(); err != nil {
return err
}
defer wc.Logout()
port, err := strconv.Atoi(config.PDU.DevicePort)
if err != nil {
return err
}
if err := wc.Loads(port, pdu.Cycle); err != nil {
return err
}
return nil
}
func run(ctx context.Context, config *Config) error {
if config.PDU != nil {
defer func() {
log.Printf("rebooting the node \"%s\"\n", config.Nodename)
if err := rebootDevice(ctx, config); 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, kernel, ramdisk, flag.Args())
}()
select {
case err := <-errs:
return err
case <-signals:
cancel()
}
return nil
}
func main() {
flag.Parse()
ctx := context.Background()
config, err := loadConfig(ctx, config)
if err != nil {
log.Fatal(err)
}
if err := run(ctx, config); err != nil {
log.Fatal(err)
}
}