blob: 6d29cc9259f95a8c7089c57cabefaaccf3b73ede [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 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)
}
}