blob: bc2857d866174d7ff9435b05a2e763d3442dfb79 [file] [log] [blame]
// 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"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"syscall"
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"
)
var (
imagesManifest string
deviceConfigPath string
bootserverPath string
)
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")
}
func rebootDevices(ctx context.Context, devices []*devicePkg.DeviceTarget) {
errs := make(chan error)
defer close(errs)
for _, device := range devices {
go func() {
errs <- device.Restart(ctx)
}()
}
for i := 0; i < len(devices); i++ {
if err := <-errs; err != nil {
log.Printf("%s\n", err)
}
}
}
// Runs a subprocess and sets up a handler that propagates SIGTERM on context cancel.
func runSubprocess(ctx context.Context, command []string) int {
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()
}
func runNUCServer(ctx context.Context, devices []*devicePkg.DeviceTarget) (*http.Server, error) {
// Parse the port from the environment variable.
portStr := os.Getenv(portEnvVar)
if portStr == "" {
return nil, nil
}
port, err := strconv.Atoi(portStr)
if err != nil {
return nil, err
}
mux := http.NewServeMux()
for _, device := range devices {
endpoint := fmt.Sprintf("/%s.ipxe", device.Mac())
mux.HandleFunc(endpoint, 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)
})
}
srv := &http.Server{
Addr: fmt.Sprintf(":%d", port),
Handler: mux,
}
go srv.ListenAndServe()
return srv, nil
}
func runBootservers(ctx context.Context, devices []*devicePkg.DeviceTarget) int {
// Execute bootserver for each node
exitCodes := make(chan int)
for _, device := range devices {
go func(device *devicePkg.DeviceTarget) {
exitCodes <- runSubprocess(ctx, device.BootserverCmd)
}(device)
}
// Wait for all of the bootservers to finish running and ensure success
numErrs := 0
for i := 0; i < len(devices); i++ {
if exitCode := <-exitCodes; exitCode != 0 {
log.Printf("bootserver exited with exit code: %d\n", exitCode)
numErrs += 1
}
}
if numErrs > 0 {
return failureExitCode
}
return 0
}
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.DeviceTargets for each of the devices in the config file
devices, err := devicePkg.CreateDeviceTargets(ctx, deviceConfigPath, bootserverCmdStub)
if err != nil {
return failureExitCode, err
}
// Set up a NUC server. If the port environment variable is not set, this is a no-op.
srv, err := runNUCServer(ctx, devices)
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)
}
}
rebootDevices(ctx, devices)
}()
if exitCode := runBootservers(ctx, devices); exitCode != 0 {
return exitCode, nil
}
// 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():
}
}()
exitCode, err := execute(ctx, flag.Args())
if err != nil || exitCode != 0 {
log.Printf("Exit code: %d, Err: %s\n", exitCode, err)
}
os.Exit(exitCode)
}