| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can |
| // found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "context" |
| "flag" |
| "fmt" |
| "io" |
| "log" |
| "net/http" |
| "net/url" |
| "os" |
| "os/exec" |
| "os/signal" |
| "path/filepath" |
| "strconv" |
| "strings" |
| "syscall" |
| "time" |
| |
| "cloud.google.com/go/storage" |
| devicePkg "go.fuchsia.dev/infra/devices" |
| ) |
| |
| const ( |
| failureExitCode = 1 |
| portEnvVar = "NUC_SERVER_PORT" |
| // chainloaderExitStr instructs a NUC to exit IPXE and boot from disk |
| chainloaderExitStr = "#!ipxe\necho Catalyst chain loader\nexit 0\n" |
| rebootDuration = 1 * time.Minute |
| zedbootPath = "zedboot.zbi" |
| ) |
| |
| var ( |
| imagesManifest string |
| deviceConfigPath string |
| bootserverPath string |
| disableRebootServer bool |
| ) |
| |
| func init() { |
| flag.StringVar(&imagesManifest, "images", "", "Path to the images manifest json") |
| flag.StringVar(&deviceConfigPath, "config", "", "Path to the device config file") |
| flag.StringVar(&bootserverPath, "bootserver", "", "Path to the bootserver binary") |
| flag.BoolVar(&disableRebootServer, "disable-reboot-server", false, "Disables the NUC soft reboot server") |
| } |
| |
| // Runs a subprocess and sets up a handler that propagates SIGTERM on context cancel. |
| func runSubprocess(ctx context.Context, command []string) int { |
| if len(command) == 0 { |
| return 0 |
| } |
| cmd := exec.Command(command[0], command[1:]...) |
| |
| // Spin off handler to exit subprocesses cleanly via SIGTERM. |
| processDone := make(chan bool, 1) |
| go func() { |
| select { |
| case <-processDone: |
| case <-ctx.Done(): |
| if err := cmd.Process.Signal(syscall.SIGTERM); err != nil { |
| log.Printf("exited cmd with error %v", err) |
| } |
| } |
| }() |
| |
| // Ensure that the context still exists before running the subprocess. |
| if ctx.Err() != nil { |
| return failureExitCode |
| } |
| |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| cmd.Run() |
| processDone <- true |
| return cmd.ProcessState.ExitCode() |
| } |
| |
| // runNUCServer runs an http server that delivers chainloaders/images to NUCs based on their mac. |
| // These chainloaders deliver a build's version of zedboot and an exit chainloader (to allow reboot from disk). |
| func runNUCServer(ctx context.Context, devices []devicePkg.Device, disabled bool) (*http.Server, error) { |
| // Parse the port from the environment variable. |
| portStr := os.Getenv(portEnvVar) |
| if portStr == "" || disabled { |
| return nil, nil |
| } |
| port, err := strconv.Atoi(portStr) |
| if err != nil { |
| return nil, err |
| } |
| |
| mux := http.NewServeMux() |
| for _, device := range devices { |
| // Add endpoint for exit chainloader. This allows NUCs to reboot from disk. |
| mux.HandleFunc(fmt.Sprintf("/%s.ipxe", device.Mac()), func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.ipxe", device.Mac())) |
| chainloader := strings.NewReader(chainloaderExitStr) |
| io.Copy(w, chainloader) |
| }) |
| |
| // Add single-use endpoint to deliver build's version of zedboot. |
| zedbootFile, err := os.Open(zedbootPath) |
| if err != nil { |
| zedbootFile = nil |
| } |
| mux.HandleFunc(fmt.Sprintf("/zedboot/%s", device.Mac()), func(w http.ResponseWriter, r *http.Request) { |
| if zedbootFile == nil { |
| w.WriteHeader(http.StatusNotFound) |
| return |
| } |
| w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", device.Mac())) |
| io.Copy(w, zedbootFile) |
| zedbootFile.Close() |
| zedbootFile = nil |
| }) |
| } |
| |
| srv := &http.Server{ |
| Addr: fmt.Sprintf(":%d", port), |
| Handler: mux, |
| } |
| go srv.ListenAndServe() |
| return srv, nil |
| } |
| |
| // prepDevices puts all devices into TaskState. |
| func prepDevices(ctx context.Context, devices []devicePkg.Device) error { |
| // Put each device into TaskState. |
| errorChannel := make(chan error) |
| for _, device := range devices { |
| go func(device devicePkg.Device) { |
| errorChannel <- device.ToTaskState(ctx) |
| }(device) |
| } |
| for i := 0; i < len(devices); i++ { |
| if err := <-errorChannel; err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // hasNUC returns true iff this testbed contains a NUC. |
| func hasNUC(devices []devicePkg.Device) bool { |
| for _, device := range devices { |
| if _, ok := device.(*devicePkg.Nuc); ok { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // downloadZedboot retrieves zedboot.zbi from GCS so that we can use the proper image |
| // in NUCs. |
| func downloadZedboot(ctx context.Context, imageURL *url.URL, devices []devicePkg.Device) error { |
| // Ensure that we have a NUC that needs a local zedboot.zbi. |
| if !hasNUC(devices) { |
| return nil |
| } |
| |
| // If we are not getting images from GCS, then this is a no-op. |
| if imageURL.Scheme != "gs" { |
| return nil |
| } |
| // Connect to GCS. |
| bucket := imageURL.Host |
| client, err := storage.NewClient(ctx) |
| if err != nil { |
| return err |
| } |
| bkt := client.Bucket(bucket) |
| |
| // Construct path to zedboot given images manifest path. |
| gcsZedbootPath := strings.TrimLeft( |
| fmt.Sprintf("%s/%s", filepath.Dir(imageURL.Path), zedbootPath), "/", |
| ) |
| |
| // Get reader to GCS object. |
| r, err := bkt.Object(gcsZedbootPath).NewReader(ctx) |
| if err != nil { |
| return err |
| } |
| defer r.Close() |
| |
| // Open a local zedboot file and download remote data into it. |
| file, err := os.Create(zedbootPath) |
| if err != nil { |
| return err |
| } |
| defer file.Close() |
| |
| if _, err := io.Copy(file, r); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| func execute(ctx context.Context, subcommandArgs []string) (int, error) { |
| // If this is a QEMU test bench, skip the device setup and just run the subprocess. |
| if deviceConfigPath == "" { |
| return runSubprocess(ctx, subcommandArgs), nil |
| } |
| // Contains all necessary bootserver flags except device nodename. |
| bootserverCmdStub := []string{ |
| bootserverPath, |
| "--images", imagesManifest, |
| "--mode", "pave-zedboot", |
| "-n", |
| } |
| |
| // Create devicePkg.Devices for each of the devices in the config file |
| configs, err := devicePkg.LoadDeviceConfigs(deviceConfigPath) |
| if err != nil { |
| return failureExitCode, err |
| } |
| devices, err := devicePkg.CreateDevices(ctx, configs, bootserverCmdStub) |
| if err != nil { |
| return failureExitCode, err |
| } |
| |
| // If the image manifest provided is a gcs url and we have a NUC, we need to download |
| // zedboot. If the image manifest is a file path, then zedboot.zbi should exist in this |
| // directory. |
| imageURL, err := url.Parse(imagesManifest) |
| if err != nil { |
| return failureExitCode, err |
| } |
| if imageURL.Scheme == "gs" { |
| // This is a no-op if no NUCs exist on this testbed. |
| if err := downloadZedboot(ctx, imageURL, devices); err != nil { |
| return failureExitCode, err |
| } |
| } |
| |
| // Set up a NUC server. If the port environment variable is not set, or if |
| // the disableRebootServer flag is set, this is a no-op. |
| srv, err := runNUCServer(ctx, devices, disableRebootServer) |
| if err != nil { |
| return failureExitCode, err |
| } |
| |
| // Clean up after tests |
| defer func() { |
| if srv != nil { |
| if err := srv.Shutdown(ctx); err != nil { |
| log.Printf("NUC server shutdown failed: %v", err) |
| } |
| } |
| }() |
| |
| if err := prepDevices(ctx, devices); err != nil { |
| return failureExitCode, err |
| } |
| |
| time.Sleep(rebootDuration) |
| // Execute the passed in subcommand |
| return runSubprocess(ctx, subcommandArgs), nil |
| } |
| |
| func main() { |
| // Initialize |
| flag.Parse() |
| |
| // Handle SIGTERM |
| ctx, cancel := context.WithCancel(context.Background()) |
| defer cancel() |
| signals := make(chan os.Signal) |
| defer func() { |
| signal.Stop(signals) |
| close(signals) |
| }() |
| signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) |
| |
| go func() { |
| select { |
| case <-signals: |
| cancel() |
| case <-ctx.Done(): |
| } |
| }() |
| |
| // Resolve environment variables within the subcommand's command line, as |
| // Swarming does not do it automatically, and best to have this done once and |
| // uniformly across all possible test task commands. |
| var subcmdArgs []string |
| for _, arg := range flag.Args() { |
| subcmdArgs = append(subcmdArgs, os.ExpandEnv(arg)) |
| } |
| exitCode, err := execute(ctx, subcmdArgs) |
| |
| if err != nil { |
| log.Printf("Exit code: %d, Err: %s\n", exitCode, err) |
| } |
| os.Exit(exitCode) |
| } |