blob: 6e0a9533c71cddcab1f2ea141e42eddd1be07fbe [file] [log] [blame]
// Copyright 2018 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 upgrade
import (
"context"
"flag"
"fmt"
"log"
"math/rand"
"os"
"testing"
"time"
"go.fuchsia.dev/fuchsia/src/sys/pkg/tests/system-tests/check"
"go.fuchsia.dev/fuchsia/src/sys/pkg/tests/system-tests/flash"
"go.fuchsia.dev/fuchsia/src/sys/pkg/tests/system-tests/pave"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/artifacts"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/device"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/errutil"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/ffx"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/sl4f"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/util"
"go.fuchsia.dev/fuchsia/tools/lib/color"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
)
var c *config
func TestMain(m *testing.M) {
log.SetPrefix("upgrade-test: ")
log.SetFlags(log.Ldate | log.Ltime | log.LUTC | log.Lshortfile)
var err error
c, err = newConfig(flag.CommandLine)
if err != nil {
log.Fatalf("failed to create config: %s", err)
}
flag.Parse()
if err = c.validate(); err != nil {
log.Fatalf("config is invalid: %s", err)
}
os.Exit(m.Run())
}
func TestOTA(t *testing.T) {
ctx := context.Background()
l := logger.NewLogger(
logger.TraceLevel,
color.NewColor(color.ColorAuto),
os.Stdout,
os.Stderr,
"upgrade-test: ")
l.SetFlags(logger.Ldate | logger.Ltime | logger.LUTC | logger.Lshortfile)
ctx = logger.WithLogger(ctx, l)
if err := doTest(ctx); err != nil {
logger.Errorf(ctx, "test failed: %v", err)
errutil.HandleError(ctx, c.deviceConfig.SerialSocketPath, err)
t.Fatal(err)
}
}
func doTest(ctx context.Context) error {
defer c.installerConfig.Shutdown(ctx)
outputDir, outputCleanup, err := c.archiveConfig.OutputDir()
if err != nil {
return fmt.Errorf("failed to get output directory: %w", err)
}
defer outputCleanup()
ffx, ffxCleanup, err := c.ffxConfig.NewFfxTool(ctx)
if err != nil {
return fmt.Errorf("failed to create ffx: %w", err)
}
defer ffxCleanup()
deviceClient, err := c.deviceConfig.NewDeviceClient(ctx, ffx)
if err != nil {
return fmt.Errorf("failed to create ota test client: %w", err)
}
defer deviceClient.Close()
// Now that we're connected to the device we can emit logs with the
// estimated device monotonic time.
l := logger.NewLogger(
logger.TraceLevel,
color.NewColor(color.ColorAuto),
os.Stdout,
os.Stderr,
device.NewEstimatedMonotonicTime(deviceClient, "upgrade-test: "),
)
l.SetFlags(logger.Ldate | logger.Ltime | logger.LUTC | logger.Lshortfile)
ctx = logger.WithLogger(ctx, l)
chainedBuilds, err := c.chainedBuildConfig.GetBuilds(ctx, deviceClient, outputDir)
if err != nil {
return fmt.Errorf("failed to get builds: %w", err)
}
for i, build := range chainedBuilds {
// FIXME(https://fxbug.dev/336897946): We need to use the latest ffx because
// F11's ffx doesn't actually refresh metadata. We can remove this once
// we cut the next stepping stone.
logger.Infof(ctx, "Refreshing TUF metadata in build %s with latest ffx", build)
repo, err := build.GetPackageRepository(ctx, artifacts.PrefetchBlobs, ffx.IsolateDir())
if err != nil {
return fmt.Errorf("error getting repository: %w", err)
}
if err := repo.RefreshMetadataWithFfx(ctx, ffx); err != nil {
return fmt.Errorf("failed to refresh TUF metadata latest ffx: %w", err)
}
// Adapt the build for the installer.
build, err = c.installerConfig.ConfigureBuild(ctx, deviceClient, build)
if err != nil {
return fmt.Errorf("failed to configure build for device: %w", err)
}
if build == nil {
return fmt.Errorf("installer did not configure a build")
}
chainedBuilds[i] = build
}
if len(chainedBuilds) == 0 {
return nil
}
// Use a seeded random source so the OTA test is consistent across runs.
rand := rand.New(rand.NewSource(99))
// Generate OTAs for each build.
otas, err := newOtas(ctx, rand, ffx, chainedBuilds)
if err != nil {
return err
}
ch := make(chan *sl4f.Configuration, 1)
if err := util.RunWithTimeout(ctx, c.paveTimeout, func() error {
currentBootSlot, err := initializeDevice(ctx, deviceClient, ffx, otas[0])
ch <- currentBootSlot
return err
}); err != nil {
err = fmt.Errorf("device failed to initialize: %w", err)
errutil.HandleError(ctx, c.deviceConfig.SerialSocketPath, err)
return err
}
currentBootSlot := <-ch
return testOTAs(ctx, deviceClient, ffx.IsolateDir(), otas, currentBootSlot)
}
func testOTAs(
ctx context.Context,
device *device.Client,
ffxIsolateDir ffx.IsolateDir,
otas []*otaData,
currentBootSlot *sl4f.Configuration,
) error {
// Perform the OTA cycles.
for i := uint(1); i <= c.cycleCount; i++ {
logger.Infof(ctx, "OTA Attempt Cycle %d. Time out in %s", i, c.cycleTimeout)
startTime := time.Now()
if err := util.RunWithTimeout(ctx, c.cycleTimeout, func() error {
// Actually OTA through all the builds.
for i := 1; i < len(otas); i++ {
srcOta := otas[i-1]
dstOta := otas[i]
logger.Infof(ctx, "Starting OTA Attempt from %s -> %s", srcOta, dstOta)
if err := systemOTA(
ctx,
device,
ffxIsolateDir,
srcOta,
dstOta,
currentBootSlot,
); err != nil {
return err
}
}
return nil
}); err != nil {
return fmt.Errorf("OTA Attempt %d failed: %w", i, err)
}
logger.Infof(ctx, "OTA cycle %d sucessful in %s", i, time.Now().Sub(startTime))
}
return nil
}
func initializeDevice(
ctx context.Context,
device *device.Client,
ffx *ffx.FFXTool,
ota *otaData,
) (*sl4f.Configuration, error) {
logger.Infof(ctx, "Initializing device")
startTime := time.Now()
systemImage, err := ota.updatePackage.OpenSystemImagePackage(ctx)
if err != nil {
return nil, err
}
// Only pave if the device is not running the expected version.
upToDate, err := check.IsDeviceUpToDate(ctx, device, systemImage)
if err != nil {
return nil, fmt.Errorf("failed to check if up to date during initialization: %w", err)
}
if !c.installerConfig.NeedsInitialization() && upToDate {
logger.Infof(ctx, "device already up to date")
} else {
sshPrivateKey, err := c.deviceConfig.SSHPrivateKey()
if err != nil {
return nil, fmt.Errorf("failed to get ssh key: %w", err)
}
if c.useFlash {
if err := flash.FlashDevice(ctx, device, ffx, ota.build, sshPrivateKey.PublicKey()); err != nil {
return nil, fmt.Errorf("failed to flash device during initialization: %w", err)
}
} else {
if err := pave.PaveDevice(ctx, device, ffx, ota.build, sshPrivateKey.PublicKey()); err != nil {
return nil, fmt.Errorf("failed to pave device during initialization: %w", err)
}
}
}
// We always boot into the A partition after initialization.
config := sl4f.ConfigurationA
currentBootSlot := &config
if err := check.ValidateDevice(
ctx,
device,
systemImage,
currentBootSlot,
c.checkABR,
); err != nil {
return nil, fmt.Errorf("failed to validate during initialization: %w", err)
}
logger.Infof(ctx, "initialization successful in %s", time.Now().Sub(startTime))
return currentBootSlot, nil
}
func systemOTA(
ctx context.Context,
device *device.Client,
ffxIsolateDir ffx.IsolateDir,
srcOta *otaData,
dstOta *otaData,
currentBootSlot *sl4f.Configuration,
) error {
var err error
// We should use this ffx after we reboot.
nextFfxTool, err := dstOta.build.GetFfx(ctx, ffxIsolateDir)
if err != nil {
return fmt.Errorf("failed to get ffx from build %s: %w", dstOta, err)
}
// Attempt an N-1 -> N OTA, up to downgradeOTAAttempts times.
// We optionally retry this OTA because some downgrade builds contain bugs which make them
// spuriously reboot. Those builds are already cut, but we still need to test them.
// See https://fxbug.dev/42061177 for more details.
for attempt := uint(1); attempt <= c.downgradeOTAAttempts; attempt++ {
logger.Infof(
ctx,
"starting OTA from %s -> %s test, attempt %d of %d",
srcOta,
dstOta,
attempt,
c.downgradeOTAAttempts,
)
otaTime := time.Now()
if err = otaToPackage(
ctx,
nextFfxTool,
device,
dstOta,
currentBootSlot,
!c.buildExpectUnknownFirmware,
); err == nil {
logger.Infof(
ctx,
"OTA from %s -> %s successful in %s",
srcOta,
dstOta,
time.Now().Sub(otaTime),
)
return nil
}
logger.Warningf(
ctx,
"OTA from %s -> %s failed, trying again %d times: %v",
srcOta,
dstOta,
c.downgradeOTAAttempts-attempt,
err,
)
// Reset our client state since the device has _potentially_
// rebooted
device.Close()
newClient, err := c.deviceConfig.NewDeviceClient(ctx, nextFfxTool)
if err != nil {
return fmt.Errorf("failed to create ota test client: %w", err)
}
*device = *newClient
}
return fmt.Errorf(
"OTA from %s -> %s failed after %d attempts: Last error: %w",
srcOta,
dstOta,
c.downgradeOTAAttempts,
err,
)
}
func otaToPackage(
ctx context.Context,
nextFfxTool *ffx.FFXTool,
device *device.Client,
ota *otaData,
currentBootSlot *sl4f.Configuration,
checkForUnknownFirmware bool,
) error {
u, err := c.installerConfig.Updater(checkForUnknownFirmware)
if err != nil {
return fmt.Errorf("failed to create updater: %w", err)
}
systemImage, err := ota.updatePackage.OpenSystemImagePackage(ctx)
if err != nil {
return err
}
if err := u.Update(ctx, nextFfxTool, device, ota.updatePackage); err != nil {
return fmt.Errorf("failed to download OTA: %w", err)
}
logger.Infof(ctx, "Validating device")
if currentBootSlot != nil {
switch *currentBootSlot {
case sl4f.ConfigurationA:
*currentBootSlot = sl4f.ConfigurationB
case sl4f.ConfigurationB:
*currentBootSlot = sl4f.ConfigurationA
case sl4f.ConfigurationRecovery:
return fmt.Errorf("device should not be in ABR recovery")
}
}
if err := check.ValidateDevice(
ctx,
device,
systemImage,
currentBootSlot,
c.checkABR,
); err != nil {
return fmt.Errorf("failed to validate after OTA: %w", err)
}
return nil
}