blob: c1c8184abfc2d9a5c6b28839d4ab639e50bb6c2e [file] [log] [blame]
// Copyright 2020 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 updater
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"regexp"
"time"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/avb"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/omaha_tool"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/packages"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/util"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/zbi"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
"golang.org/x/crypto/ssh"
)
const (
updateErrorSleepTime = 30 * time.Second
// The default fuchsia update package path.
defaultUpdatePackagePath = "update/0"
)
type client interface {
ExpectReboot(ctx context.Context, f func() error) error
Reboot(ctx context.Context) error
DisconnectionListener() <-chan struct{}
ServePackageRepository(
ctx context.Context,
repo *packages.Repository,
name string,
createRewriteRule bool,
rewritePackages []string) (*packages.Server, error)
Run(ctx context.Context, command []string, stdout io.Writer, stderr io.Writer) error
}
type Updater interface {
Update(
ctx context.Context,
c client,
updatePackage *packages.UpdatePackage,
) error
}
func checkSyslogForUnknownFirmware(ctx context.Context, c client) error {
logger.Infof(ctx, "Checking system log for errors")
// Try to dump logs using the new logger.
var stdout bytes.Buffer
if err := c.Run(
ctx,
[]string{"log_listener", "--tag", "system-updater", "dump"},
&stdout,
os.Stderr,
); err != nil {
// Don't bother trying to fall back to the old command if we
// disconnected from the device.
var errExitMissing *ssh.ExitMissingError
if errors.As(err, &errExitMissing) {
return err
}
// Otherwise fall back to the old logger
stdout = bytes.Buffer{}
if err := c.Run(
ctx,
[]string{"log_listener", "--tag", "system-updater", "--dump_logs", "yes"},
&stdout,
os.Stderr,
); err != nil {
return err
}
}
re := regexp.MustCompile("skipping unsupported .* type:")
scanner := bufio.NewScanner(&stdout)
for scanner.Scan() {
line := scanner.Text()
if re.MatchString(line) {
return fmt.Errorf("System Updater should not have skipped installing firmware: %s", line)
}
}
return nil
}
// SystemUpdateChecker uses `update check-now` to install a package.
type SystemUpdateChecker struct {
updatePackage *packages.UpdatePackage
checkForUnkownFirmware bool
}
func NewSystemUpdateChecker(checkForUnkownFirmware bool) *SystemUpdateChecker {
return &SystemUpdateChecker{
checkForUnkownFirmware: checkForUnkownFirmware,
}
}
func (u *SystemUpdateChecker) Update(
ctx context.Context,
c client,
srcUpdatePackage *packages.UpdatePackage,
) error {
// If we're using the default update package url, we can directly update
// with it.
if srcUpdatePackage.Path() == defaultUpdatePackagePath {
return updateCheckNow(
ctx,
c,
srcUpdatePackage.Repository(),
true,
u.checkForUnkownFirmware,
)
}
// Otherwise, copy the repository into a temporary directory, and publish
// the update package to `fuchsia-pkg://fuchsia.com/update/0`, then update
// with the temp repository.
logger.Infof(
ctx,
"update package %s isn't default, cloning the repository so it can be made %s",
srcUpdatePackage,
defaultUpdatePackagePath,
)
tempDir, err := os.MkdirTemp("", "")
if err != nil {
return fmt.Errorf("failed to create a temp directory: %w", err)
}
defer os.RemoveAll(tempDir)
tempRepo, err := srcUpdatePackage.Repository().CloneIntoDir(ctx, tempDir)
if err != nil {
return fmt.Errorf(
"failed to copy %s into %s: %w",
srcUpdatePackage.Repository(),
tempDir,
err,
)
}
tempSrcUpdate, err := tempRepo.OpenUpdatePackage(ctx, srcUpdatePackage.Path())
if err != nil {
return fmt.Errorf("failed to open %s: %w", srcUpdatePackage, err)
}
_, err = tempSrcUpdate.EditContents(ctx, defaultUpdatePackagePath, func(tempDir string) error { return nil })
if err != nil {
return fmt.Errorf("failed to publish %s: %w", defaultUpdatePackagePath, err)
}
return updateCheckNow(ctx, c, tempRepo, true, u.checkForUnkownFirmware)
}
func updateCheckNow(
ctx context.Context,
c client,
repo *packages.Repository,
createRewriteRule bool,
checkForUnkownFirmware bool,
) error {
logger.Infof(ctx, "Triggering OTA")
startTime := time.Now()
err := c.ExpectReboot(ctx, func() error {
// Since an update can trigger a reboot, we can run into all
// sorts of races. The two main ones are:
//
// * the network connection is torn down before we see the
// `update` command exited cleanly.
// * the system updater service was torn down before the
// `update` process, which would show up as the channel to
// be closed.
//
// In order to avoid this races, we need to:
//
// * assume the ssh connection was closed means the OTA was
// probably installed and the device rebooted as normal.
// * `update` exiting with a error could be we just lost the
// shutdown race. So if we get an `update` error, wait a few
// seconds to see if the device disconnects. If so, treat it
// like the OTA was successful.
// We pass createRewriteRule=true for versions of system-update-checker prior to
// fxrev.dev/504000. Newer versions need to have `update channel set` called below.
server, err := c.ServePackageRepository(ctx, repo, "trigger-ota", createRewriteRule, nil)
if err != nil {
return fmt.Errorf("error setting up server: %w", err)
}
defer server.Shutdown(ctx)
ch := c.DisconnectionListener()
cmd := []string{
"/bin/update",
"channel",
"set",
"trigger-ota",
}
if err := c.Run(ctx, cmd, os.Stdout, os.Stderr); err != nil {
logger.Warningf(ctx, "update channel set failed: %v. This probably indicates the device is running an old version of system-update-checker.", err)
}
cmd = []string{
"/bin/update",
"check-now",
"--monitor",
}
err = c.Run(ctx, cmd, os.Stdout, os.Stderr)
if err == nil && checkForUnkownFirmware {
// FIXME(https://fxbug.dev/42077484): We wouldn't have to ignore disconnects
// if we could trigger an update without it automatically rebooting.
err = checkSyslogForUnknownFirmware(ctx, c)
}
if err != nil {
// If the device rebooted before ssh was able to tell
// us the command ran, it will tell us the session
// exited without passing along an exit code. So,
// ignore that specific error.
var errExitMissing *ssh.ExitMissingError
if errors.As(err, &errExitMissing) {
logger.Warningf(ctx, "disconnected, assuming this was because OTA triggered reboot")
return nil
}
logger.Warningf(ctx, "update errored out, but maybe it lost the race, waiting a moment to see if the device reboots: %v", err)
// We got an error, but maybe we lost the reboot race.
// So wait a few moments to see if the device reboots
// anyway.
select {
case <-ch:
logger.Warningf(ctx, "disconnected, assuming this was because OTA triggered reboot")
return nil
case <-time.After(updateErrorSleepTime):
return fmt.Errorf("failed to trigger OTA: %w", err)
}
}
return nil
})
if err != nil {
return err
}
cmd := []string{"/bin/update", "wait-for-commit"}
if err := c.Run(ctx, cmd, os.Stdout, os.Stderr); err != nil {
logger.Warningf(ctx, "update wait-for-commit failed: %v", err)
}
logger.Infof(ctx, "OTA completed in %s", time.Now().Sub(startTime))
return nil
}
// SystemUpdater uses the `system-updater` to install a package.
type SystemUpdater struct {
checkForUnkownFirmware bool
}
func NewSystemUpdater(checkForUnkownFirmware bool) *SystemUpdater {
return &SystemUpdater{
checkForUnkownFirmware: checkForUnkownFirmware,
}
}
func (u *SystemUpdater) Update(
ctx context.Context,
c client,
srcUpdate *packages.UpdatePackage,
) error {
startTime := time.Now()
repoName := "download-ota"
dstUpdate, err := srcUpdate.RehostUpdatePackage(
ctx,
repoName,
util.AddSuffixToPackageName(srcUpdate.Path(), "system-updater"),
)
server, err := c.ServePackageRepository(ctx, dstUpdate.Repository(), repoName, true, nil)
if err != nil {
return fmt.Errorf("error setting up server: %w", err)
}
defer server.Shutdown(ctx)
updatePackageUrl := fmt.Sprintf("fuchsia-pkg://%s/%s", repoName, dstUpdate.Path())
logger.Infof(ctx, "Downloading OTA %q", updatePackageUrl)
cmd := []string{
"update",
"force-install",
"--reboot", "false",
fmt.Sprintf("%q", updatePackageUrl),
}
if err := c.Run(ctx, cmd, os.Stdout, os.Stderr); err != nil {
return fmt.Errorf("failed to run system updater: %w", err)
}
logger.Infof(ctx, "OTA successfully downloaded in %s", time.Now().Sub(startTime))
if err := checkSyslogForUnknownFirmware(ctx, c); err != nil {
return err
}
logger.Infof(ctx, "Rebooting device")
startTime = time.Now()
if err = c.Reboot(ctx); err != nil {
return fmt.Errorf("device failed to reboot after OTA applied: %w", err)
}
logger.Infof(ctx, "Reboot complete in %s", time.Now().Sub(startTime))
cmd = []string{"/bin/update", "wait-for-commit"}
if err := c.Run(ctx, cmd, os.Stdout, os.Stderr); err != nil {
logger.Warningf(ctx, "update wait-for-commit failed: %v", err)
}
return nil
}
type OmahaUpdater struct {
omahaTool *omaha_tool.OmahaTool
avbTool *avb.AVBTool
zbiTool *zbi.ZBITool
workaroundOtaNoRewriteRules bool
checkForUnkownFirmware bool
}
func NewOmahaUpdater(
omahaTool *omaha_tool.OmahaTool,
avbTool *avb.AVBTool,
zbiTool *zbi.ZBITool,
workaroundOtaNoRewriteRules bool,
checkForUnkownFirmware bool,
) *OmahaUpdater {
return &OmahaUpdater{
omahaTool: omahaTool,
avbTool: avbTool,
zbiTool: zbiTool,
workaroundOtaNoRewriteRules: workaroundOtaNoRewriteRules,
checkForUnkownFirmware: checkForUnkownFirmware,
}
}
func (u *OmahaUpdater) Update(
ctx context.Context,
c client,
srcUpdate *packages.UpdatePackage,
) error {
logger.Infof(ctx, "injecting omaha_url into %q", srcUpdate)
// Create a ZBI with the omaha_url argument.
destZbi, err := os.CreateTemp("", "omaha_argument.zbi")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
defer os.Remove(destZbi.Name())
imageArguments := map[string]string{
"omaha_url": u.omahaTool.URL(),
"omaha_app_id": u.omahaTool.Args.AppId,
}
logger.Infof(ctx, "Omaha Server URL set in vbmeta to %q", u.omahaTool.URL())
if err := u.zbiTool.MakeImageArgsZbi(ctx, destZbi.Name(), imageArguments); err != nil {
return fmt.Errorf("failed to create ZBI: %w", err)
}
// Create a vbmeta that includes the ZBI we just created.
propFiles := map[string]string{
"zbi": destZbi.Name(),
}
repoName := "trigger-ota"
dstUpdate, err := srcUpdate.EditUpdatePackageWithVBMetaProperties(
ctx,
u.avbTool,
repoName,
util.AddSuffixToPackageName(srcUpdate.Path(), "omaha-client"),
propFiles,
)
if err != nil {
return fmt.Errorf("failed to inject vbmeta properties into update package: %w", err)
}
omahaPackageURL := fmt.Sprintf(
"fuchsia-pkg://%s/%s?hash=%s",
repoName,
dstUpdate.Path(),
dstUpdate.Merkle(),
)
logger.Infof(ctx, "Update Package URL: %q", omahaPackageURL)
// Configure the Omaha server with the new omaha package URL.
if err := u.omahaTool.SetPkgURL(ctx, omahaPackageURL); err != nil {
return fmt.Errorf("Failed to set Omaha update package: %w", err)
}
// Trigger an update
return updateCheckNow(ctx, c, dstUpdate.Repository(), !u.workaroundOtaNoRewriteRules, u.checkForUnkownFirmware)
}