blob: 4c29c0952c7c306d5992bf5a02387c5042456590 [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 firmwareservice
import (
"context"
"fmt"
"log"
"net/url"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"unicode"
"go.chromium.org/chromiumos/test/provision/lib/servo_lib"
"go.chromium.org/chromiumos/test/provision/lib/servoadapter"
common_utils "go.chromium.org/chromiumos/test/provision/v2/common-utils"
"github.com/pkg/errors"
conf "go.chromium.org/chromiumos/config/go"
"go.chromium.org/chromiumos/config/go/test/api"
"gopkg.in/yaml.v3"
)
// FirmwareVersions holds the ro and rw versions read from a firmware binary.
type FirmwareVersions struct {
AP struct {
Versions struct {
RO string `yaml:"ro"`
RW string `yaml:"rw"`
} `yaml:"versions"`
} `yaml:"host"`
EC struct {
Versions struct {
RO string `yaml:"ro"`
RW string `yaml:"rw"`
RWHash string
} `yaml:"versions"`
} `yaml:"ec"`
}
// FirmwareService implements ServiceInterface
type FirmwareService struct {
// In case of flashing over SSH, |connection| connects to the DUT.
// In case of flashing over Servo, |connection| connects to the ServoHost.
connection common_utils.ServiceAdapterInterface
// DetailedRequest fields
mainRwPath *conf.StoragePath
mainRoPath *conf.StoragePath
ecRoPath *conf.StoragePath
board, model string
// The name of the model's fw binary from config.yaml
CorebootName string
ECName string
force bool
ecChip string
// imagesMetadata is a map from gspath -> ImageArchiveMetadata.
// Allows to avoid redownloading/reprocessing archives.
imagesMetadata map[string]ImageArchiveMetadata
// DUTServer is the gRPC connection to the DUT.
DUTServer api.DutServiceClient
useServo bool
// servoConfig provides dut-controls and programmer argument for flashing
servoConfig *servo_lib.ServoConfig
// wrapper around servoClient connection with extra servod-related functions
servoConnection servoadapter.ServoHostInterface
servoPort int
CacheServer url.URL
ExpectedVersions FirmwareVersions
// RestartRequired indicates that a firmware update was performed, but the DUT has not yet rebooted.
RestartRequired bool
}
type versionJSON struct {
Default FirmwareVersions `yaml:"default"`
}
// NewFirmwareService initializes a FirmwareService.
func NewFirmwareService(ctx context.Context, dutServer api.DutServiceClient,
servoClient api.ServodServiceClient, cacheServer url.URL, board, model string,
useServo bool, req *api.InstallRequest) (*FirmwareService, error) {
metadata := new(api.FirmwareProvisionInstallMetadata)
if req.GetMetadata().MessageIs(metadata) {
if err := req.GetMetadata().UnmarshalTo(metadata); err != nil {
return nil, InvalidRequestErr(errors.Wrap(err, "unmarshalling metadata"))
}
}
detailedRequest := metadata.FirmwareConfig
dutAdapter := common_utils.NewServiceAdapter(dutServer, false /*noReboot*/)
fws := FirmwareService{
connection: dutAdapter,
DUTServer: dutServer,
board: board,
model: model,
force: false,
useServo: useServo,
imagesMetadata: make(map[string]ImageArchiveMetadata),
CacheServer: cacheServer,
}
// Firmware may be updated in write-protected mode, where only 'rw' regions
// would be update, or write-protection may be disabled (dangerous) in order
// to update 'ro' regions.
//
// The only 'rw' firmware is the main one, aka AP firmware, which also syncs
// EC 'rw'. It will be flashed with write protection turned on.
fws.mainRwPath = detailedRequest.MainRwPayload.GetFirmwareImagePath()
// Read-only firmware images include AP, EC, and PD firmware, and will be
// flashed with write protection turned off.
fws.mainRoPath = detailedRequest.MainRoPayload.GetFirmwareImagePath()
fws.ecRoPath = detailedRequest.EcRoPayload.GetFirmwareImagePath()
if useServo {
fws.prepareServoConnection(ctx, servoClient)
}
fws.PrintRequestInfo()
if !fws.UpdateRo() && !fws.UpdateRw() {
return nil, InvalidRequestErr(errors.New("no paths to images specified"))
}
return &fws, nil
}
// Confirms that cros-servod connection is functional, and fills the following
// fields in FirmwareService:
// - servoConfig
// - servoConnection
// - ecChip
// - servoPort
func (fws *FirmwareService) prepareServoConnection(ctx context.Context, servoClient api.ServodServiceClient) error {
if servoClient == nil {
return InvalidRequestErr(errors.New("servo use is requested, but servo client not provided"))
}
// Note: dut.GetChromeos().Servo.ServodAddress.Port is the port of cros-servod
// service, we seem to be missing port of servod itself.
// Always use default servod port 9999 for now.
if fws.servoPort == 0 {
fws.servoPort = 9999
}
fws.servoConnection = servoadapter.NewServoHostAdapterFromExecCmder(fws.board,
fws.model,
fws.servoPort,
servoClient)
// Ask servod for servo_type. This implicitly checks if servod is running
// and servo connection is working.
servoTypeStr, err := fws.servoConnection.GetVariable(ctx, "servo_type")
if err != nil {
return UnreachablePreProvisionErr(errors.Wrapf(err, "failed to get servo_type. "+
"Is servod running on port %v and connected to the DUT?",
fws.servoPort)) // TODO(sfrolov): add UnreachableCrosServodErr
}
// ask servod for serial number of the connected DUT.
servoType := servo_lib.NewServoType(servoTypeStr)
if servoType.IsMultipleServos() {
// Handle dual servo.
// We need CCD if ec_ro is set, otherwise, servo_micro will be faster.
preferCCD := false
if fws.ecRoPath != nil {
preferCCD = true
}
servoSubtypeStr := servoType.PickServoSubtype(preferCCD)
servoType = servo_lib.NewServoType(servoSubtypeStr)
}
serial, err := fws.servoConnection.GetVariable(ctx, servoType.GetSerialNumberOption())
if err != nil {
return UnreachablePreProvisionErr(errors.Wrap(err, "failed to get serial number variable")) // TODO: cros-servod is unreachable
}
// finally, get the correct config
fws.servoConfig, err = servo_lib.GetServoConfig(fws.board, serial, servoType)
if err != nil {
return UnreachablePreProvisionErr(errors.Wrap(err, "failed to get servo config")) // TODO: cros-servod is unreachable
}
fws.ecChip, err = fws.servoConnection.GetVariable(ctx, "ec_chip")
if err != nil {
return UnreachablePreProvisionErr(errors.Wrap(err, "failed to get ec_chip variable")) // TODO: cros-servod is unreachable
}
return nil
}
// PrintRequestInfo logs details of the provisioning operation.
func (fws *FirmwareService) PrintRequestInfo() {
informationString := "provisioning "
images := []string{}
if fws.mainRwPath != nil {
images = append(images, "AP(RW)")
}
if fws.mainRoPath != nil {
images = append(images, "AP(RO)")
}
if fws.ecRoPath != nil {
images = append(images, "EC(RO)")
}
informationString += strings.Join(images, " and ") + " firmware"
flashMode := "SSH"
if fws.useServo {
flashMode = fmt.Sprintf("Servo %s", (fws.servoConfig.ServoType))
}
informationString += " over " + flashMode + ". "
informationString += "Board: " + fws.board + ". "
informationString += "Model: " + fws.model + ". "
informationString += fmt.Sprintf("Force: %v.", fws.force)
log.Println("[FW Provisioning]", informationString)
}
// UpdateRw returns whether the read/write firmware is being operated on.
func (fws *FirmwareService) UpdateRw() bool {
return fws.mainRwPath != nil
}
// UpdateRo returns whether the read-only firmware is being operated on.
func (fws *FirmwareService) UpdateRo() bool {
return (fws.mainRoPath != nil) || (fws.ecRoPath != nil)
}
// GetBoard returns board of the DUT to provision. Returns empty string if board is not known.
func (fws *FirmwareService) GetBoard() string {
return fws.board
}
// RestartDut restarts the DUT using one of the available mechanisms.
// Preferred restart method is to send "power_state:reset" command to servod.
// If servod restarting failed/not available in the environment,
// then this function will try to SSH to the DUT and run `restart`,
// if |requireServoReset| is false. If |requireServoReset| is True, restart over
// SSH will not be attempted, and the function will return an error.
func (fws *FirmwareService) RestartDut(ctx context.Context, requireServoReset bool) error {
waitForReconnect := func(conn common_utils.ServiceAdapterInterface) error {
// Attempts to run `true` on the DUT over SSH until |reconnectRetries| attempts.
const reconnectRetries = 10
const reconnectAttemptWait = 10 * time.Second
const reconnectFailPause = 10 * time.Second
var restartErr error
for i := 0; i < reconnectRetries; i++ {
reconnectCtx, reconnCancel := context.WithTimeout(ctx, reconnectAttemptWait)
defer reconnCancel()
_, restartErr = conn.RunCmd(reconnectCtx, "true", nil)
if restartErr == nil {
log.Println("[FW Provisioning: Restart DUT] reestablished connection to the DUT after restart.")
return nil
}
time.Sleep(reconnectFailPause)
}
log.Printf("[FW Provisioning: Restart DUT] timed out waiting for DUT to restart: %v\n", restartErr)
return restartErr
}
if requireServoReset && fws.servoConnection == nil {
return errors.New("servo restart is required but servo connection not available")
}
// over Servo first
if fws.servoConnection != nil {
log.Printf("[FW Provisioning: Restart DUT] restarting DUT with \"dut-control power_state:reset\" over servo.\n")
servoRestartErr := fws.servoConnection.RunDutControl(ctx, []string{"power_state:reset"})
if servoRestartErr == nil {
if fws.connection != nil {
// If SSH connection to DUT was available, wait until it's back up again.
return waitForReconnect(fws.connection)
}
waitDuration := 30 * time.Second
log.Printf("[FW Provisioning: Restart DUT] waiting for %v for DUT to finish rebooting.\n", waitDuration.String())
time.Sleep(waitDuration)
powerState, getPowerStateErr := fws.servoConnection.GetVariable(ctx, "ec_system_powerstate")
if getPowerStateErr != nil {
log.Printf("[FW Provisioning: Restart DUT] failed to get power state after reboot: %v\n", getPowerStateErr)
} else {
log.Printf("[FW Provisioning: Restart DUT] DUT power state after reboot: %v\n", powerState)
}
return getPowerStateErr
}
log.Printf("[FW Provisioning: Restart DUT] failed to restart DUT via Servo: %v.\n", servoRestartErr)
if requireServoReset {
return servoRestartErr
}
}
// over SSH if allowed by |requireServoReset|
if fws.connection != nil {
log.Printf("[FW Provisioning: Restart DUT] restarting DUT over SSH.")
fws.connection.Restart(ctx)
return waitForReconnect(fws.connection)
}
return errors.New("failed to restart: no SSH connection to the DUT")
}
// GetModel returns model of the DUT to provision. Returns empty string if model is not known.
func (fws *FirmwareService) GetModel() string {
return fws.model
}
// DeleteArchiveDirectories deletes files on the servo host or DUT.
func (fws *FirmwareService) DeleteArchiveDirectories() error {
var cleanedDevice common_utils.ServiceAdapterInterface
if fws.useServo {
// If servo is used, the files will be located on the ServoHost.
cleanedDevice = fws.servoConnection
} else {
// If SSH is used, the files will be located on the DUT in /tmp/
// It's not strictly necessary to delete them before reboot.
cleanedDevice = fws.connection
}
var allErrors []string
for _, imgMetadata := range fws.imagesMetadata {
err := cleanedDevice.DeleteDirectory(context.Background(), imgMetadata.ArchiveDir)
if err != nil {
allErrors = append(allErrors, fmt.Sprintf("failed to delete %v: %v", imgMetadata.ArchiveDir, err))
}
}
if len(allErrors) > 0 {
return errors.New(strings.Join(allErrors, ". "))
}
return nil
}
// FlashWithFutility flashes the DUT using "futility" tool.
// futility will be run with "--mode=recovery".
// if |rwOnly| is true, futility will flash only RW regions.
// if |rwOnly| is false, futility will flash both RW and RO regions.
// futilityArgs must include argument(s) that provide path(s) to the images.
// apImagePath is the path to the AP image.
// ecImagePath is the path to the EC image.
//
// If flashing over ssh, simply calls runFutility().
// If flashing over servo, also runs pre- and post-flashing dut-controls.
func (fws *FirmwareService) FlashWithFutility(ctx context.Context, rwOnly bool, futilityImageArgs []string, apImagePath, ecImagePath string) error {
if err := fws.ExtractFirmwareVersions(ctx, rwOnly, futilityImageArgs, apImagePath); err != nil {
return errors.Wrap(err, "extract versions")
}
if fws.useServo {
return fws.servoFlash(ctx, rwOnly, futilityImageArgs)
}
return fws.sshFlash(ctx, rwOnly, futilityImageArgs, ecImagePath)
}
// ExtractFirmwareVersions uses futility to get the version numbers from the images.
func (fws *FirmwareService) ExtractFirmwareVersions(ctx context.Context, rwOnly bool, futilityImageArgs []string, apImagePath string) error {
if len(futilityImageArgs) == 0 {
return errors.New("unable to gte version: no futility Image args provided")
}
futilityArgs := futilityImageArgs
futilityArgs = append([]string{"update", "--manifest"}, futilityArgs...)
connection := fws.GetConnectionToFlashingDevice()
out, err := connection.RunCmd(ctx, "futility", futilityArgs)
if err != nil {
return errors.Wrap(err, "failed to exec futility")
}
versions := versionJSON{}
err = yaml.Unmarshal([]byte(out), &versions)
if err != nil {
return errors.Wrap(err, "failed to parse futility manifest")
}
if !rwOnly && versions.Default.AP.Versions.RO != "" {
fws.ExpectedVersions.AP.Versions.RO = versions.Default.AP.Versions.RO
}
if versions.Default.AP.Versions.RW != "" {
fws.ExpectedVersions.AP.Versions.RW = versions.Default.AP.Versions.RW
}
if !rwOnly && versions.Default.EC.Versions.RO != "" {
fws.ExpectedVersions.EC.Versions.RO = versions.Default.EC.Versions.RO
}
if versions.Default.EC.Versions.RW != "" {
fws.ExpectedVersions.EC.Versions.RW = versions.Default.EC.Versions.RW
}
if apImagePath != "" {
// Get the EC RW hash from the AP image, since the version in the manifest may be wrong.
ecImagePath := fmt.Sprintf("%s-ecrw.hash", apImagePath)
_, err = connection.RunCmd(ctx, "cbfstool", []string{apImagePath, "extract", "-r", "FW_MAIN_A", "-n", "ecrw.hash", "-f", ecImagePath})
if err != nil {
return errors.Wrap(err, "failed to extract ecrw.hash")
}
out, err = connection.RunCmd(ctx, "od", []string{"-A", "n", "-t", "x1", ecImagePath})
if err != nil {
return errors.Wrap(err, "failed to get ecrw hash")
}
fws.ExpectedVersions.EC.Versions.RW = ""
fws.ExpectedVersions.EC.Versions.RWHash = strings.Join(strings.FieldsFunc(out, unicode.IsSpace), "")
}
return nil
}
// GetVersions returns a FirmwareProvisionResponse with the expected firmware versions
func (fws *FirmwareService) GetVersions() *api.FirmwareProvisionResponse {
r := new(api.FirmwareProvisionResponse)
r.ApRoVersion = fws.ExpectedVersions.AP.Versions.RO
r.ApRwVersion = fws.ExpectedVersions.AP.Versions.RW
r.EcRoVersion = fws.ExpectedVersions.EC.Versions.RO
r.EcRwVersion = fws.ExpectedVersions.EC.Versions.RW
return r
}
// ActiveFirmwareVersions returns the firmware versions currently running on the DUT.
func (fws *FirmwareService) ActiveFirmwareVersions(ctx context.Context) (*FirmwareVersions, error) {
var versions FirmwareVersions
// Run crossystem ro_fwid fwid to get AP versions
out, err := fws.connection.RunCmd(ctx, "crossystem", []string{"ro_fwid", "fwid"})
if err != nil {
return nil, errors.Wrap(err, "crossystem failed")
}
apVers := strings.SplitN(out, " ", 2)
if len(apVers) != 2 {
return nil, errors.Errorf("incorrect crossystem output: %q", out)
}
versions.AP.Versions.RO = apVers[0]
versions.AP.Versions.RW = apVers[1]
// Run ectool version to get EC versions
out, err = fws.connection.RunCmd(ctx, "ectool", []string{"version"})
if err != nil {
// Wilco, etc.
if strings.Contains(out, "Very likely this board doesn't have a Chromium EC") {
return &versions, nil
}
return nil, errors.Wrap(err, "ectool failed")
}
roRE, err := regexp.Compile(`RO version:\s*(\S+)\s*\n`)
if err != nil {
return nil, errors.Wrap(err, "invalid roRE")
}
rwRE, err := regexp.Compile(`RW version:\s*(\S+)\s*\n`)
if err != nil {
return nil, errors.Wrap(err, "invalid rwRE")
}
if m := roRE.FindStringSubmatch(out); m != nil {
versions.EC.Versions.RO = m[1]
} else {
return nil, errors.Errorf("EC RO version not found in %q", out)
}
if m := rwRE.FindStringSubmatch(out); m != nil {
versions.EC.Versions.RW = m[1]
} else {
return nil, errors.Errorf("EC RW version not found in %q", out)
}
// Run ectool echash to get EC RW hash
out, err = fws.connection.RunCmd(ctx, "ectool", []string{"echash"})
if err != nil {
// Wilco, etc.
if strings.Contains(out, "Very likely this board doesn't have a Chromium EC") {
return &versions, nil
}
return nil, errors.Wrap(err, "ectool failed")
}
statusRE, err := regexp.Compile(`status:\s*(\S+)\s*\n`)
if err != nil {
return nil, errors.Wrap(err, "invalid statusRE")
}
hashRE, err := regexp.Compile(`hash:\s*(\S+)\s*\n`)
if err != nil {
return nil, errors.Wrap(err, "invalid hashRE")
}
if m := statusRE.FindStringSubmatch(out); m == nil || m[1] != "done" {
return nil, errors.Errorf("EC hash not done in %q", out)
}
if m := hashRE.FindStringSubmatch(out); m != nil {
versions.EC.Versions.RWHash = m[1]
} else {
return nil, errors.Errorf("EC RW hash not found in %q", out)
}
return &versions, nil
}
func (fws *FirmwareService) sshFlash(ctx context.Context, rwOnly bool, futilityImageArgs []string, ecImagePath string) error {
return fws.runFutility(ctx, rwOnly, futilityImageArgs, ecImagePath)
}
func (fws *FirmwareService) servoFlash(ctx context.Context, rwOnly bool, futilityImageArgs []string) error {
dutOnErr := fws.servoConnection.RunAllDutControls(ctx, fws.servoConfig.DutOn)
if dutOnErr != nil {
return errors.Wrap(dutOnErr, "failed to run pre-flashing dut-controls")
}
flashErr := fws.runFutility(ctx, rwOnly, futilityImageArgs, "")
// Attempt to run post-flashing dut-controls regardless of flashErr.
dutOffErr := fws.servoConnection.RunAllDutControls(ctx, fws.servoConfig.DutOff)
if flashErr != nil {
return flashErr
}
if dutOffErr != nil {
return errors.Wrap(dutOffErr, "failed to run post-flashing dut-controls")
}
return nil
}
const (
// The character class \w is equivalent to [0-9A-Za-z_]. Leading equals sign is unsafe in zsh,
// see http://zsh.sourceforge.net/Doc/Release/Expansion.html#g_t_0060_003d_0027-expansion.
leadingSafeChars = `-\w@%+:,./`
trailingSafeChars = leadingSafeChars + "="
)
// safeRE matches an argument that can be literally included in a shell
// command line without requiring escaping.
var safeRE = regexp.MustCompile(fmt.Sprintf("^[%s][%s]*$", leadingSafeChars, trailingSafeChars))
// escape escapes a string so it can be safely included as an argument in a shell command line.
// The string is not modified if it can already be safely included.
func escape(s string) string {
if safeRE.MatchString(s) {
return s
}
return "'" + strings.Replace(s, "'", `'"'"'`, -1) + "'"
}
func (fws *FirmwareService) runFutility(ctx context.Context, rwOnly bool, futilityImageArgs []string, ecImagePath string) error {
if len(futilityImageArgs) == 0 {
return errors.New("unable to flash: no futility Image args provided")
}
futilityArgs := futilityImageArgs
futilityArgs = append([]string{"update", "--mode=recovery"}, futilityArgs...)
if rwOnly {
futilityArgs = append(futilityArgs, "--wp=1")
} else {
futilityArgs = append(futilityArgs, "--wp=0")
}
if fws.force {
futilityArgs = append(futilityArgs, "--force")
}
if fws.useServo {
futilityArgs = append(futilityArgs, "-p", fws.servoConfig.Programmer)
futilityArgs = append(futilityArgs, fws.servoConfig.ExtraArgs...)
// TODO(sfrolov): extra args from fw-config.json
}
fws.RestartRequired = true
connection := fws.GetConnectionToFlashingDevice()
if ecImagePath != "" {
// If we are flashing EC, we might lose SSH access, so use nohup to run futility in a temporary python script, and poll for results
logFile := path.Join(path.Dir(ecImagePath), "futility.log")
pyScript := path.Join(path.Dir(ecImagePath), "futility.py")
var scriptBody strings.Builder
scriptBody.WriteString(`#!/usr/bin/env python3
import subprocess
with open("`)
scriptBody.WriteString(logFile)
scriptBody.WriteString(`", "wb", buffering=0) as outFile:
rc = subprocess.run(["futility"`)
for _, arg := range futilityArgs {
scriptBody.WriteString(", '")
scriptBody.WriteString(arg)
scriptBody.WriteString("'")
}
scriptBody.WriteString(`], stdout=outFile, stderr=outFile, check=False, bufsize=0)
outFile.write(f"EXIT CODE: {rc.returncode}\n".encode("utf-8"))
outFile.flush()
subprocess.run(["sync", "-d", "/var/tmp", "/usr/local/tmp"])
subprocess.run(["reboot"])
`)
_, _, err := RunDUTCommand(ctx, fws.DUTServer, time.Minute, "cat", []string{">", pyScript}, []byte(scriptBody.String()))
if err != nil {
return errors.Wrap(err, "failed to create futility.py")
}
defer connection.RunCmd(ctx, "rm", []string{"-f", logFile})
_, err = connection.RunCmd(ctx, "bash", []string{"-c", escape(fmt.Sprintf("nohup python3 %s </dev/null >&/dev/null & exit", pyScript))})
if err != nil {
return errors.Wrap(err, "failed to run nohup")
}
startTime := time.Now()
exitCodeRe := regexp.MustCompile(`EXIT CODE: (-?\d+)`)
// Poll every 10s for the log file to appear and have the "EXIT CODE:" string in it.
for time.Now().Sub(startTime) < 5*time.Minute {
time.Sleep(10 * time.Second)
buf, err := connection.RunCmd(ctx, "cat", []string{logFile})
if err != nil {
log.Printf("failed to download %q: %v", logFile, err)
continue
}
m := exitCodeRe.FindStringSubmatch(buf)
if m != nil {
log.Printf("Futility output:\n%s", buf)
if m[1] == "0" {
fws.RestartRequired = false
return nil
}
return errors.Errorf("futility failed: %q", buf)
} else {
log.Printf("Exit code not found in %q\n", buf)
}
}
return errors.New("Timeout waiting for futility")
} else {
if _, err := connection.RunCmd(ctx, "futility", futilityArgs); err != nil {
return err
}
}
return nil
}
// GetConnectionToFlashingDevice returns connection to the device that stores the
// firmware image and runs futility.
// Returns connection to ServoHost if fws.useServo, connection to DUT otherwise.
func (fws *FirmwareService) GetConnectionToFlashingDevice() common_utils.ServiceAdapterInterface {
if fws.useServo {
return fws.servoConnection
}
return fws.connection
}
// IsServoUsed returns whether servo is used during the provisioning.
func (fws *FirmwareService) IsServoUsed() bool {
return fws.useServo
}
// IsForceUpdate returns whether an update is forced during the provisioning.
func (fws *FirmwareService) IsForceUpdate() bool {
return fws.force
}
// GetMainRwPath returns the path of the read/write part of the AP firmware.
func (fws *FirmwareService) GetMainRwPath() string {
return fws.mainRwPath.GetPath()
}
// GetMainRoPath returns the path of readonly part of the AP firmware.
func (fws *FirmwareService) GetMainRoPath() string {
return fws.mainRoPath.GetPath()
}
// GetEcRoPath returns the path for the readonly portion of the EC firmware.
func (fws *FirmwareService) GetEcRoPath() string {
return fws.ecRoPath.GetPath()
}
type configData struct {
ChromeOS struct {
Configs []struct {
Firmware struct {
BuildTargets struct {
// AP image name, if missing fallback to ImageName
Coreboot string `yaml:"coreboot"`
EC string `yaml:"ec"`
ZephyrEC string `yaml:"zephyr-ec"`
} `yaml:"build-targets"`
ImageName string `yaml:"image-name"`
} `yaml:"firmware"`
Name string `yaml:"name"`
Identity struct {
SKUID int `yaml:"sku-id"`
} `yaml:"identity"`
}
}
}
// ReadConfigYAML downloads config.yaml from the DUT and find the firmware binary names that should be used.
func (fws *FirmwareService) ReadConfigYAML(ctx context.Context) error {
out, err := fws.connection.RunCmd(ctx, "crosid", nil)
if err != nil {
return errors.Wrap(err, "failed to run crosid")
}
re, err := regexp.Compile(`^SKU='([^']*)'`)
if err != nil {
return errors.Wrap(err, "sku regex failed")
}
m := re.FindStringSubmatch(out)
sku := -1
if m != nil {
sku, err = strconv.Atoi(m[1])
if err != nil {
return errors.Wrapf(err, "parse of %q failed", m[1])
}
log.Printf("DUT sku = %d", sku)
}
yamlPath := "/usr/share/chromeos-config/yaml/config.yaml"
ok, err := fws.connection.PathExists(ctx, yamlPath)
if err != nil {
return errors.Wrap(err, "failed to check config.yaml")
} else if !ok {
log.Printf("No config.yaml found on DUT at %q", yamlPath)
return nil
}
config, err := fws.connection.FetchFile(ctx, yamlPath)
if err != nil {
return errors.Wrapf(err, "failed to read %q", yamlPath)
}
defer config.Close()
configYaml := configData{}
parser := yaml.NewDecoder(config)
err = parser.Decode(&configYaml)
if err != nil {
return errors.Wrap(err, "failed to parse config.yaml")
}
model := fws.GetModel()
for _, config := range configYaml.ChromeOS.Configs {
if config.Name == model {
if sku >= 0 && config.Identity.SKUID >= 0 && config.Identity.SKUID != sku {
continue
}
thisAPName := config.Firmware.BuildTargets.Coreboot
if thisAPName == "" {
thisAPName = config.Firmware.ImageName
}
if thisAPName != "" {
if fws.CorebootName != "" && fws.CorebootName != thisAPName {
return errors.Errorf("ambiguous AP name for model %q sku %d, could be %q or %q", model, sku, fws.CorebootName, thisAPName)
}
fws.CorebootName = thisAPName
}
thisECName := config.Firmware.BuildTargets.ZephyrEC
if thisECName == "" {
thisECName = config.Firmware.BuildTargets.EC
}
if thisECName != "" {
if fws.ECName != "" && fws.ECName != thisECName {
return errors.Errorf("ambiguous EC name for model %q sku %d, could be %q or %q", model, sku, fws.ECName, thisECName)
}
fws.ECName = thisECName
}
}
}
log.Printf("config.yaml image names AP: %s EC: %s", fws.CorebootName, fws.ECName)
return nil
}
// DownloadAndProcess gets ready to extract the selected archive.
// It doesn't actually do any downloading and should be renamed.
func (fws *FirmwareService) DownloadAndProcess(ctx context.Context, gsPath string) error {
connection := fws.GetConnectionToFlashingDevice()
if _, alreadyProcessed := fws.imagesMetadata[gsPath]; !alreadyProcessed {
// Infer names for the local files and folders from basename of gsPath.
archiveFilename := filepath.Base(gsPath)
// Try to get a descriptive name for the temporary folder.
archiveSubfolder := archiveFilename
if strings.HasPrefix(archiveSubfolder, "firmware_from_source") {
splitGsPath := strings.Split(gsPath, "/")
nameIdx := len(splitGsPath) - 2
if nameIdx < 0 {
nameIdx = 0
}
archiveSubfolder = splitGsPath[nameIdx]
}
// Use mktemp to safely create a unique temp directory in /var/tmp so that it survives reboots.
archiveDir, err := connection.RunCmd(ctx, "mktemp", []string{"-d", "--tmpdir=/var/tmp", "cros-fw-provision.XXXXXXXXX." + archiveSubfolder})
if err != nil {
return errors.Wrap(err, "remote mktemp failed")
}
archiveDir = strings.Trim(archiveDir, "\n")
fws.imagesMetadata[gsPath] = ImageArchiveMetadata{
ArchiveDir: archiveDir,
}
}
return nil
}
// GetImageMetadata returns (ImageArchiveMetadata, IsImageMetadataPresent)
func (fws *FirmwareService) GetImageMetadata(gspath string) (ImageArchiveMetadata, bool) {
metadata, ok := fws.imagesMetadata[gspath]
return metadata, ok
}
// ProvisionWithFlashEC flashes EC image using flash_ec script.
func (fws *FirmwareService) ProvisionWithFlashEC(ctx context.Context, ecImage, flashECScriptPath string) error {
customBitbangRate := ""
if fws.ecChip == "stm32" {
customBitbangRate = "--bitbang_rate=57600"
}
flashCmdArgs := fmt.Sprintf("--ro --chip=%s --board=%s --image=%s --port=%v %s --verify --verbose",
fws.ecChip, fws.model, ecImage, fws.servoPort, customBitbangRate)
output, err := fws.GetConnectionToFlashingDevice().RunCmd(ctx, flashECScriptPath, strings.Split(flashCmdArgs, " "))
if err != nil {
log.Println(output)
return err
}
return nil
}