blob: 324bf2f6e4b9723372412964bee465b5c82bd741 [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"
"errors"
"fmt"
"log"
"strings"
"time"
"go.chromium.org/chromiumos/test/provision/cmd/provisionserver/bootstrap/services"
"go.chromium.org/chromiumos/test/provision/lib/servo_lib"
"go.chromium.org/chromiumos/test/provision/lib/servoadapter"
conf "go.chromium.org/chromiumos/config/go"
"go.chromium.org/chromiumos/config/go/test/api"
lab_api "go.chromium.org/chromiumos/config/go/test/lab/api"
)
// FirmwareService inherits 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 services.ServiceAdapterInterface
mainRwPath *conf.StoragePath
mainRoPath *conf.StoragePath
ecRoPath *conf.StoragePath
board, model string
force bool
ecChip string
// imagesMetadata is a map from gspath -> ImageArchiveMetadata.
// Allows to avoid redownloading/reprocessing archives.
imagesMetadata map[string]ImageArchiveMetadata
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
}
func NewFirmwareService(ctx context.Context, dut *lab_api.Dut, dutClient api.DutServiceClient, servoClient api.ServodServiceClient, req *api.InstallFirmwareRequest) (*FirmwareService, error) {
connection := services.NewServiceAdapter(dut, dutClient, false /*noReboot*/)
return NewFirmwareServiceFromExistingConnection(ctx, dut, connection, servoClient, req)
}
// NewFirmwareServiceFromExistingConnection is equivalent to the above constructor,
// but recycles a ServiceAdapter. Generally useful for tests.
func NewFirmwareServiceFromExistingConnection(ctx context.Context, dut *lab_api.Dut, connection services.ServiceAdapterInterface, servoClient api.ServodServiceClient, req *api.InstallFirmwareRequest) (*FirmwareService, error) {
if req.FirmwareConfig == nil {
return nil, errors.New("request.FirmwareConfig is nil")
}
// 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.
mainRwPath := req.FirmwareConfig.MainRwPayload.GetFirmwareImagePath()
// Read-only firmware images include AP, EC, and PD firmware, and will be
// flashed with write protection turned off.
mainRoPath := req.FirmwareConfig.MainRoPayload.GetFirmwareImagePath()
ecRoPath := req.FirmwareConfig.EcRoPayload.GetFirmwareImagePath()
imagesMetadata := make(map[string]ImageArchiveMetadata)
board := dut.GetChromeos().GetDutModel().GetBuildTarget()
if len(board) == 0 {
return nil, errors.New("\"board\" field in Dut proto is required")
}
model := dut.GetChromeos().GetDutModel().GetModelName()
if len(model) == 0 {
return nil, errors.New("\"model\" field in Dut proto is required")
}
force := req.GetForce()
useServo := req.GetUseServo()
var servoConfig *servo_lib.ServoConfig
var servoConnection servoadapter.ServoHostInterface
var ecChip string
var servoPort int
if useServo {
if servoClient == nil {
return nil, fmt.Errorf("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.
if servoPort == 0 {
servoPort = 9999
}
servoConnection = servoadapter.NewServoHostAdapterFromExecCmder(board, model, servoPort, servoClient)
// Ask servod for servo_type. This implicitly checks if servod is running
// and servo connection is working.
servoTypeStr, err := servoConnection.GetVariable(ctx, "servo_type")
if err != nil {
return nil, fmt.Errorf("failed to get servo_type: %w. "+
"Is servod running on port %v and connected to the DUT?",
err, servoPort)
}
// 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 req.FirmwareConfig.EcRoPayload.GetFirmwareImagePath() != nil {
preferCCD = true
}
servoSubtypeStr := servoType.PickServoSubtype(preferCCD)
servoType = servo_lib.NewServoType(servoSubtypeStr)
}
serial, err := servoConnection.GetVariable(ctx, servoType.GetSerialNumberOption())
if err != nil {
return nil, err
}
// finally, get the correct config
servoConfig, err = servo_lib.GetServoConfig(board, serial, servoType)
if err != nil {
return nil, err
}
ecChip, err = servoConnection.GetVariable(ctx, "ec_chip")
if err != nil {
return nil, err
}
}
fws := FirmwareService{
connection,
mainRwPath,
mainRoPath,
ecRoPath,
board,
model,
force,
ecChip,
imagesMetadata,
useServo,
servoConfig,
servoConnection,
servoPort,
}
fws.PrintRequestInfo()
if !fws.UpdateRo() && !fws.UpdateRw() {
return nil, errors.New("request.FirmwareConfig: no paths to images specified")
}
return &fws, nil
}
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)
}
func (fws *FirmwareService) UpdateRw() bool {
return fws.mainRwPath != nil
}
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 services.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)
} else {
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
}
// GetFirstState returns the first state of this state machine
func (fws *FirmwareService) GetFirstState() services.ServiceState {
return FirmwarePrepareState{
service: *fws,
}
}
// CleanupOnFailure is called if one of service's states failes to Execute() and
// currenly cleans up the temporary files.
func (fws *FirmwareService) CleanupOnFailure(states []services.ServiceState, executionErr error) error {
return fws.deleteArchiveDirectories()
}
func (fws *FirmwareService) deleteArchiveDirectories() error {
var cleanedDevice services.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.
//
// 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) error {
if fws.useServo {
return fws.servoFlash(ctx, rwOnly, futilityImageArgs)
} else {
return fws.sshFlash(ctx, rwOnly, futilityImageArgs)
}
}
func (fws FirmwareService) sshFlash(ctx context.Context, rwOnly bool, futilityImageArgs []string) error {
return fws.runFutility(ctx, rwOnly, futilityImageArgs)
}
func (fws FirmwareService) servoFlash(ctx context.Context, rwOnly bool, futilityImageArgs []string) error {
dutOnErr := fws.servoConnection.RunAllDutControls(ctx, fws.servoConfig.DutOn)
if dutOnErr != nil {
return fmt.Errorf("failed to run pre-flashing dut-controls: %w", dutOnErr)
}
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 fmt.Errorf("failed to run post-flashing dut-controls: %w", dutOffErr)
}
return nil
}
func (fws FirmwareService) runFutility(ctx context.Context, rwOnly bool, futilityImageArgs []string) error {
if len(futilityImageArgs) == 0 {
return fmt.Errorf("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
}
connection := fws.GetConnectionToFlashingDevice()
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() services.ServiceAdapterInterface {
if fws.useServo {
return fws.servoConnection
} else {
return fws.connection
}
}
// 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
}