| // Copyright 2019 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 ( |
| "context" |
| "errors" |
| "flag" |
| "fmt" |
| "io" |
| "net" |
| "os" |
| "strings" |
| "time" |
| |
| "go.fuchsia.dev/fuchsia/tools/bootserver" |
| "go.fuchsia.dev/fuchsia/tools/build" |
| "go.fuchsia.dev/fuchsia/tools/lib/color" |
| "go.fuchsia.dev/fuchsia/tools/lib/logger" |
| "go.fuchsia.dev/fuchsia/tools/lib/retry" |
| "go.fuchsia.dev/fuchsia/tools/net/netboot" |
| "go.fuchsia.dev/fuchsia/tools/net/netutil" |
| "go.fuchsia.dev/fuchsia/tools/net/tftp" |
| ) |
| |
| const ( |
| bootloaderVersion = "0.7.22" |
| defaultMicrosecBetweenPackets = 20 |
| nodenameEnvKey = "ZIRCON_NODENAME" |
| retryDelay = time.Second |
| ) |
| |
| // Firmware arguments have variable names based on their type e.g.: |
| // |
| // --firmware=<file> |
| // --firmware-foo=<file> |
| // |
| // The flag package doesn't support anything like this, so we have to do a bit |
| // of extra work to find any of these args and add them to the list. |
| type firmwareArg struct { |
| name string |
| value string |
| fwType string |
| help string |
| } |
| |
| var ( |
| bootOnce bool |
| bootIpv6 string |
| tftpBlockSize int |
| packetInterval int |
| nodename string |
| windowSize int |
| boardName string |
| bootKernel string |
| fvm string |
| bootloader string |
| firmware []firmwareArg |
| zircona string |
| zirconb string |
| zirconr string |
| vbmetaa string |
| vbmetab string |
| vbmetar string |
| authorizedKeysFile string |
| failFast bool |
| useNetboot bool |
| useTftp bool |
| nocolor bool |
| allowZedbootVersionMismatch bool |
| failFastZedbootVersionMismatch bool |
| imageManifest string |
| mode bootserver.Mode |
| // TODO(ihuh): Remove when no longer used. This is not actually used |
| // anywhere. It is just here to hold the value of the --no-bind flag. |
| noBind bool |
| logLevel = logger.InfoLevel |
| |
| // errIncompleteTransfer represents a failure to transfer pave images |
| // to the device. |
| errIncompleteTransfer = errors.New("transfer incomplete") |
| ) |
| |
| func init() { |
| // Support classic cmd line interface. |
| flag.StringVar(&nodename, "n", "", "only boot device with this nodename") |
| flag.StringVar(&bootKernel, "boot", "", "use the supplied file as a kernel") |
| flag.StringVar(&fvm, "fvm", "", "use the supplied file as a sparse FVM image (up to 4 times)") |
| flag.StringVar(&bootloader, "bootloader", "", "use the supplied file as a bootloader image") |
| flag.StringVar(&zircona, "zircona", "", "use the supplied file as a zircon-a zbi") |
| flag.StringVar(&zirconb, "zirconb", "", "use the supplied file as a zircon-b zbi") |
| flag.StringVar(&zirconr, "zirconr", "", "use the supplied file as a zircon-r zbi") |
| flag.StringVar(&vbmetaa, "vbmetaa", "", "use the supplied file as a avb vbmeta_a image") |
| flag.StringVar(&vbmetab, "vbmetab", "", "use the supplied file as a avb vbmeta_b image") |
| flag.StringVar(&vbmetar, "vbmetar", "", "use the supplied file as a avb vbmeta_r image") |
| |
| // Support firmware arguments. |
| firmware = getFirmwareArgs(os.Args) |
| for i := range firmware { |
| flag.StringVar(&firmware[i].value, firmware[i].name, "", firmware[i].help) |
| } |
| |
| // Support reading in images.json and paving zedboot. |
| flag.StringVar(&imageManifest, "images", "", "use an image manifest to pave") |
| flag.Var(&mode, "mode", "bootserver modes: either pave, netboot, or pave-zedboot") |
| |
| flag.StringVar(&bootIpv6, "a", "", "only boot device with this IPv6 address") |
| flag.BoolVar(&allowZedbootVersionMismatch, "allow-zedboot-version-mismatch", false, "warn on zedboot version mismatch rather than fail") |
| flag.StringVar(&authorizedKeysFile, "authorized-keys", "", "use the supplied file as an authorized_keys file") |
| flag.IntVar(&tftpBlockSize, "b", 0, "tftp block size") |
| flag.StringVar(&boardName, "board_name", "", "name of the board files are meant for") |
| flag.BoolVar(&bootOnce, "1", true, "only boot once, then exit") |
| flag.BoolVar(&failFast, "fail-fast", false, "exit on first error") |
| flag.BoolVar(&failFastZedbootVersionMismatch, "fail-fast-if-version-mismatch", false, "error if zedboot version does not match") |
| // TODO(ihuh): Remove once all uses of this flag are removed. This is already true by default if -a or -n are provided. |
| flag.BoolVar(&noBind, "no-bind", false, "do not bind to bootserver port. Should be used with -a <IPV6>") |
| flag.IntVar(&windowSize, "w", 0, "tftp window size, ignored with --netboot") |
| |
| // TODO(https://fxbug.dev/42114276): Implement the following unsupported flags. |
| flag.IntVar(&packetInterval, "i", defaultMicrosecBetweenPackets, "number of microseconds between packets; ignored with --tftp") |
| // We currently always default to tftp |
| flag.BoolVar(&useNetboot, "netboot", false, "use the netboot protocol") |
| flag.BoolVar(&useTftp, "tftp", true, "use the tftp protocol (default)") |
| flag.BoolVar(&nocolor, "nocolor", false, "disable ANSI color (false)") |
| flag.Var(&logLevel, "log-level", "log level, can be fatal, error, warning, info, debug, or trace (default info)") |
| } |
| |
| // Creates a slice of FirmwareArgs from |args|. |
| func getFirmwareArgs(args []string) []firmwareArg { |
| // Always include the default --firmware whether specified or not, to give |
| // a useful help message. |
| fwArgs := []firmwareArg{{ |
| name: "firmware", |
| value: "", |
| fwType: "", |
| help: "use the supplied file as the default firmware image; use --firmware-<type> for typed images", |
| }} |
| |
| for _, arg := range args { |
| // Go supports one or two dash prefixes. |
| fwType := strings.TrimPrefix(arg, "-firmware") |
| fwType = strings.TrimPrefix(fwType, "--firmware") |
| if fwType == arg { |
| // Didn't find a prefix match, on to the next arg. |
| continue |
| } |
| |
| // If the caller used "arg=value", peel off the "=value" part as we're |
| // not actually parsing, but just finding all the necessary arg names. |
| fwType = strings.SplitN(fwType, "=", 2)[0] |
| |
| if fwType == "" { |
| // We already added the default untyped "--firmware" arg. |
| } else if fwType[0] == '-' { |
| fwType = fwType[1:] |
| fwArgs = append(fwArgs, firmwareArg{ |
| name: fmt.Sprintf("firmware-%s", fwType), |
| value: "", |
| fwType: fwType, |
| help: fmt.Sprintf("use the supplied file as the %q firmware image", fwType), |
| }) |
| } |
| |
| // If we got here it wasn't a firmware arg, just ignore it. |
| } |
| |
| return fwArgs |
| } |
| |
| func overrideImage(ctx context.Context, imgMap map[string]bootserver.Image, img bootserver.Image) map[string]bootserver.Image { |
| if existing, ok := imgMap[img.Name]; ok { |
| if existing.Reader != nil { |
| if closer, ok := existing.Reader.(io.Closer); ok { |
| if err := closer.Close(); err != nil { |
| logger.Warningf(ctx, "failed to close image %s: %v", existing.Name, err) |
| } |
| } |
| } |
| } |
| imgMap[img.Name] = img |
| return imgMap |
| } |
| |
| func getImages(ctx context.Context) ([]bootserver.Image, func() error, error) { |
| imgMap := make(map[string]bootserver.Image) |
| // If an image manifest is provided, we use that. |
| if imageManifest != "" { |
| imgs, closeFunc, err := bootserver.GetImages(ctx, imageManifest, mode) |
| if err != nil { |
| return imgs, closeFunc, err |
| } |
| for _, img := range imgs { |
| imgMap[img.Name] = img |
| } |
| } |
| // If cmdline args are provided, append them to the image list, overriding any |
| // images from the manifest. |
| if bootKernel != "" { |
| imgMap = overrideImage(ctx, imgMap, bootserver.Image{ |
| Image: build.Image{ |
| Name: "zbi_netboot", |
| Path: bootKernel, |
| }, |
| Args: []string{"--boot"}, |
| }) |
| } |
| if bootloader != "" { |
| imgMap = overrideImage(ctx, imgMap, bootserver.Image{ |
| Image: build.Image{ |
| Name: "blk_efi", |
| Path: bootloader, |
| }, |
| Args: []string{"--bootloader"}, |
| }) |
| } |
| for _, fw := range firmware { |
| imgMap = overrideImage(ctx, imgMap, bootserver.Image{ |
| // Trailing delimiter is OK for untyped images. |
| Image: build.Image{ |
| Name: "img_firmware_" + fw.fwType, |
| Path: fw.value, |
| }, |
| Args: []string{"--firmware-" + fw.fwType}, |
| }) |
| } |
| if fvm != "" { |
| imgMap = overrideImage(ctx, imgMap, bootserver.Image{ |
| Image: build.Image{ |
| Name: "blk_storage-sparse", |
| Path: fvm, |
| }, |
| Args: []string{"--fvm"}, |
| }) |
| } |
| if zircona != "" { |
| imgMap = overrideImage(ctx, imgMap, bootserver.Image{ |
| Image: build.Image{ |
| Name: "zbi_zircon-a", |
| Path: zircona, |
| }, |
| Args: []string{"--zircona"}, |
| }) |
| } |
| if zirconb != "" { |
| imgMap = overrideImage(ctx, imgMap, bootserver.Image{ |
| Image: build.Image{ |
| Name: "zbi_zircon-b", |
| Path: zirconb, |
| }, |
| Args: []string{"--zirconb"}, |
| }) |
| } |
| if zirconr != "" { |
| imgMap = overrideImage(ctx, imgMap, bootserver.Image{ |
| Image: build.Image{ |
| Name: "zbi_zircon-r", |
| Path: zirconr, |
| }, |
| Args: []string{"--zirconr"}, |
| }) |
| } |
| if vbmetaa != "" { |
| imgMap = overrideImage(ctx, imgMap, bootserver.Image{ |
| Image: build.Image{ |
| Name: "vbmeta_zircon-a", |
| Path: vbmetaa, |
| }, |
| Args: []string{"--vbmetaa"}, |
| }) |
| } |
| if vbmetab != "" { |
| imgMap = overrideImage(ctx, imgMap, bootserver.Image{ |
| Image: build.Image{ |
| Name: "vbmeta_zircon-b", |
| Path: vbmetab, |
| }, |
| Args: []string{"--vbmetab"}, |
| }) |
| } |
| if vbmetar != "" { |
| imgMap = overrideImage(ctx, imgMap, bootserver.Image{ |
| Image: build.Image{ |
| Name: "vbmeta_zircon-r", |
| Path: vbmetar, |
| }, |
| Args: []string{"--vbmetar"}, |
| }) |
| } |
| imgs := []bootserver.Image{} |
| for _, img := range imgMap { |
| imgs = append(imgs, img) |
| } |
| closeFunc, err := populateReaders(imgs) |
| return imgs, closeFunc, err |
| } |
| |
| func populateReaders(imgs []bootserver.Image) (func() error, error) { |
| closeFunc := func() error { |
| var errs []error |
| for _, img := range imgs { |
| if img.Reader != nil { |
| if closer, ok := img.Reader.(io.Closer); ok { |
| if err := closer.Close(); err != nil { |
| errs = append(errs, err) |
| } |
| } |
| } |
| } |
| if len(errs) > 0 { |
| return fmt.Errorf("failed to close images: %v", errs) |
| } |
| return nil |
| } |
| for i := range imgs { |
| if imgs[i].Reader != nil { |
| continue |
| } |
| r, err := os.Open(imgs[i].Path) |
| if err != nil { |
| // Close already opened readers. |
| closeFunc() |
| return closeFunc, fmt.Errorf("failed to open %s: %w", imgs[i].Path, err) |
| } |
| fi, err := r.Stat() |
| if err != nil { |
| closeFunc() |
| return closeFunc, fmt.Errorf("failed to get file info for %s: %w", imgs[i].Path, err) |
| } |
| imgs[i].Reader = r |
| imgs[i].Size = fi.Size() |
| } |
| return closeFunc, nil |
| } |
| |
| func connectAndBoot(ctx context.Context, nodename string, imgs []bootserver.Image, cmdlineArgs []string, authorizedKeys []byte) error { |
| var addr *net.UDPAddr |
| if bootIpv6 != "" { |
| ipAddr, err := net.ResolveIPAddr("ip6", bootIpv6) |
| if err != nil { |
| return fmt.Errorf("%s: invalid ipv6 address specified", bootIpv6) |
| } |
| addr = &net.UDPAddr{IP: ipAddr.IP, Zone: ipAddr.Zone} |
| } |
| addr, msg, cleanup, err := netutil.GetAdvertisement(ctx, nodename, addr) |
| if err != nil { |
| return fmt.Errorf("%w: %v", errIncompleteTransfer, err) |
| } |
| defer cleanup() |
| if msg.BootloaderVersion != bootloaderVersion { |
| mismatchErrMsg := fmt.Sprintf("WARNING: Bootserver version '%s' != remote Zedboot version '%s'", bootloaderVersion, msg.BootloaderVersion) |
| if allowZedbootVersionMismatch { |
| logger.Warningf(ctx, "%s. Paving may fail.", mismatchErrMsg) |
| } else { |
| if failFastZedbootVersionMismatch { |
| failFast = true |
| } |
| return fmt.Errorf("%w: %s. Device will not be serviced. Please upgrade Zedboot.", errIncompleteTransfer, mismatchErrMsg) |
| } |
| } |
| udpAddr := &net.UDPAddr{ |
| IP: addr.IP, |
| Port: tftp.ClientPort, |
| Zone: addr.Zone, |
| } |
| client, err := tftp.NewClient(udpAddr, uint16(tftpBlockSize), uint16(windowSize)) |
| if err != nil { |
| return fmt.Errorf("%w: %v", errIncompleteTransfer, err) |
| } |
| |
| if boardName != "" { |
| if err := bootserver.ValidateBoard(ctx, client, boardName); err != nil { |
| return err |
| } |
| } |
| |
| logger.Infof(ctx, "Proceeding with nodename %s", msg.Nodename) |
| |
| if err := bootserver.Boot(ctx, client, imgs, cmdlineArgs, authorizedKeys); err != nil { |
| return fmt.Errorf("%w: %v", errIncompleteTransfer, err) |
| } |
| return nil |
| } |
| |
| func resolveNodename() (string, error) { |
| if nodename == "" { |
| envNodename, ok := os.LookupEnv(nodenameEnvKey) |
| if ok && envNodename != "" { |
| return envNodename, nil |
| } else { |
| // Return the NodenameWildcard to discover any device. |
| return netboot.NodenameWildcard, nil |
| } |
| } |
| return nodename, nil |
| } |
| |
| func execute(ctx context.Context, cmdlineArgs []string) error { |
| // Do some secondary cmdline arg validation. |
| if imageManifest != "" && mode == bootserver.ModeNull { |
| return fmt.Errorf("must specify a bootserver mode [--mode] when using an image manifest") |
| } else if imageManifest == "" && mode != bootserver.ModeNull { |
| return fmt.Errorf("cannot specify a bootserver mode without an image manifest [--images]") |
| } |
| |
| // Remove the default firmware if the caller didn't actually use it. |
| if firmware[0].value == "" { |
| firmware = firmware[1:] |
| } |
| |
| if boardName != "" { |
| logger.Infof(ctx, "Board name set to [%s]", boardName) |
| } |
| |
| imgs, closeFunc, err := getImages(ctx) |
| if err != nil { |
| return err |
| } |
| if len(imgs) == 0 { |
| return fmt.Errorf("no images provided!") |
| } |
| defer closeFunc() |
| |
| n, err := resolveNodename() |
| if err != nil { |
| return err |
| } |
| |
| if allowZedbootVersionMismatch && failFastZedbootVersionMismatch { |
| return fmt.Errorf("only one of allow-zedboot-version-mismatch and fail-fast-if-version-mismatch can be true") |
| } |
| |
| var authorizedKeys []byte |
| if authorizedKeysFile != "" { |
| authorizedKeys, err = os.ReadFile(authorizedKeysFile) |
| if err != nil { |
| return fmt.Errorf("could not read SSH key file %q: %v", authorizedKeysFile, err) |
| } |
| } |
| |
| // Keep discovering and booting devices. |
| return retry.Retry(ctx, retry.NewConstantBackoff(retryDelay), func() error { |
| if err := connectAndBoot(ctx, n, imgs, cmdlineArgs, authorizedKeys); err != nil { |
| if bootOnce || failFast || !errors.Is(err, errIncompleteTransfer) { |
| // Exit early for any error other than an errIncompleteTransfer, |
| // or if failFast or bootOnce is enabled. |
| return retry.Fatal(err) |
| } |
| // Error is an errIncompleteTransfer. Retry after some delay. |
| logger.Errorf(ctx, "%v", err) |
| return err |
| } |
| return nil |
| }, nil) |
| } |
| |
| func main() { |
| flag.Parse() |
| |
| log := logger.NewLogger(logLevel, color.NewColor(color.ColorAuto), os.Stdout, os.Stderr, "bootserver ") |
| log.SetFlags(logger.Ltime | logger.Lmicroseconds | logger.Lshortfile) |
| |
| ctx := logger.WithLogger(context.Background(), log) |
| if err := execute(ctx, flag.Args()); err != nil { |
| logger.Fatalf(ctx, "%v", err) |
| } |
| } |