| // 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) |
| } |
| } |