blob: eb206caf554d7238f8e2b20b3c3c3212435751c3 [file] [log] [blame]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package internal
import (
"bytes"
"context"
"encoding/gob"
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"strings"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"chromium.googlesource.com/chromiumos/platform/dev-util.git/contrib/fflash/internal/dut"
embeddedagent "chromium.googlesource.com/chromiumos/platform/dev-util.git/contrib/fflash/internal/embedded-agent"
"chromium.googlesource.com/chromiumos/platform/dev-util.git/contrib/fflash/internal/payload"
"chromium.googlesource.com/chromiumos/platform/dev-util.git/contrib/fflash/internal/ssh"
)
const devFeaturesRootfsVerification = "/usr/libexec/debugd/helpers/dev_features_rootfs_verification"
const oauth2Scopes = "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/devstorage.read_only"
// getToken returns the user's token to access Google Cloud Storage.
// It uses luci-auth which is shipped with depot_tools.
func getToken(ctx context.Context) (oauth2.TokenSource, error) {
// Impersonate gsutil
// https://github.com/GoogleCloudPlatform/gsutil/blob/7bad311bd5444907c515ff745429cc2ffd31b22d/gslib/utils/system_util.py#L174
c := oauth2.Config{
ClientID: "909320924072.apps.googleusercontent.com",
ClientSecret: "p3RlpR10xMFh9ZXBS/ZNLYUu",
Endpoint: google.Endpoint,
}
cmd := exec.CommandContext(ctx, "luci-auth", "token", "-scopes", oauth2Scopes)
stdout, err := cmd.Output()
if err != nil {
if err, ok := err.(*exec.ExitError); ok {
return nil, fmt.Errorf(`luci-auth failed: %s
You may need to login with:
luci-auth login -scopes %q
refer to the error message below:
=== luci-auth output ===
%s`,
err,
oauth2Scopes,
strings.TrimSpace(string(err.Stderr)),
)
}
return nil, fmt.Errorf("luci-auth failed: %s", err)
}
ts := c.TokenSource(
ctx,
&oauth2.Token{
AccessToken: strings.TrimSpace(string(stdout)),
},
)
return ts, nil
}
type Options struct {
PayloadFile string `json:",omitempty"` // JSON payload file to flash
GS string `json:",omitempty"` // gs:// directory to flash
VersionString string `json:",omitempty"` // version string such as R105-14989.0.0
MilestoneNum int `json:",omitempty"` // release number such as 105
Board string `json:",omitempty"` // build target name such as brya
Snapshot bool `json:",omitempty"` // flash a snapshot build
Postsubmit bool `json:",omitempty"` // flash a postsubmit build
Port string `json:",omitempty"` // port number to connect to on the dut-host
DryRun bool `json:",omitempty"` // print what is going to be flashed but do not actually flash
// the ConnectTimeout of the SSH command.
SSHConnectTimeout time.Duration `json:",omitempty"`
dut.FlashOptions
}
func Main(ctx context.Context, t0 time.Time, target string, opts *Options) error {
if err := embeddedagent.SelfCheck(); err != nil {
return err
}
optionsString, err := json.Marshal(opts)
if err != nil {
panic(err) // should never fail
}
log.Printf("Running fflash with options: %s", string(optionsString))
tkSrc, err := getToken(ctx)
if err != nil {
return err
}
sshDialer, err := ssh.NewDialer(ssh.SSHOptions{Port: opts.Port, ConnectTimeout: opts.SSHConnectTimeout})
if err != nil {
return fmt.Errorf("failed to make ssh dialer: %w", err)
}
defer sshDialer.Close()
sshClient, err := sshDialer.DialWithSystemSSH(ctx, target)
if err != nil {
return fmt.Errorf("system ssh failed: %w", err)
}
defer sshClient.Close()
dutArch, err := DetectArch(sshClient)
if err != nil {
return err
}
log.Println("DUT arch:", dutArch)
var payload *payload.OneOf
if opts.PayloadFile != "" {
payload, err = createFilePayload(opts.PayloadFile)
} else {
payload, err = createGSPayload(ctx, tkSrc, sshClient, target, opts)
}
if err != nil {
return fmt.Errorf("createGSPayload(): %w", err)
}
req := &dut.Request{
Payload: payload,
FlashOptions: opts.FlashOptions,
}
if opts.DryRun {
log.Print("Exit early due to --dry-run")
return nil
}
log.Println("pushing dut-agent to", target)
bin, err := embeddedagent.ExecutableForArch(dutArch)
if err != nil {
return err
}
agentPath, err := PushCompressedExecutable(ctx, sshClient, bin)
if err != nil {
return err
}
log.Println("agent pushed to", agentPath)
session, err := sshClient.NewSession()
if err != nil {
return err
}
var stdin bytes.Buffer
req.ElapsedTimeWhenSent = time.Since(t0)
if err := gob.NewEncoder(&stdin).Encode(req); err != nil {
return fmt.Errorf("failed to write flash request: %w", err)
}
session.Stdin = &stdin
var stdout bytes.Buffer
session.Stdout = &stdout
session.Stderr = os.Stderr
if err := session.Run(agentPath); err != nil {
return fmt.Errorf("dut-agent failed: %w", err)
}
var result dut.Result
if err := gob.NewDecoder(&stdout).Decode(&result); err != nil {
return fmt.Errorf("cannot decode dut-agent result: %w", err)
}
oldParts, err := DetectPartitions(sshClient)
if err != nil {
return err
}
log.Println("DUT root is on:", oldParts.ActiveRootfs())
sshClient, err = CheckedReboot(ctx, sshDialer, sshClient, target, oldParts.InactiveRootfs())
if err != nil {
return err
}
needs2ndReboot := false
if result.RetryDisableRootfsVerification {
log.Println("retrying disable rootfs verification")
if _, err := sshClient.RunSimpleOutput(devFeaturesRootfsVerification); err != nil {
return fmt.Errorf("disable rootfs verification failed: %w", err)
}
needs2ndReboot = true
}
if result.RetryClearTpmOwner {
log.Println("retrying clear tpm owner")
if _, err := sshClient.RunSimpleOutput("crossystem clear_tpm_owner_request=1"); err != nil {
return fmt.Errorf("failed to clear tpm owner: %w", err)
}
needs2ndReboot = true
}
if needs2ndReboot {
sshClient, err = CheckedReboot(ctx, sshDialer, sshClient, target, oldParts.InactiveRootfs())
if err != nil {
return err
}
}
if opts.DisableRootfsVerification {
if _, err := sshClient.RunSimpleOutput(devFeaturesRootfsVerification + " -q"); err != nil {
return fmt.Errorf("failed to check rootfs verification: %w", err)
}
}
return nil
}
func createFilePayload(payloadFile string) (*payload.OneOf, error) {
r, err := os.Open(payloadFile)
if err != nil {
return nil, err
}
out := &payload.OneOf{}
dec := json.NewDecoder(r)
dec.DisallowUnknownFields()
if err := dec.Decode(out); err != nil {
return nil, fmt.Errorf("error parsing %q: %w", payloadFile, err)
}
return out, nil
}