blob: 46b8b628f37d6da5315aabcbbc8a093faa828ea8 [file] [log] [blame]
// Copyright 2024 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// DLC constants and helpers
package cross_over
import (
"context"
"errors"
"fmt"
"log"
"regexp"
"strings"
"time"
common_utils "go.chromium.org/chromiumos/test/provision/v2/common-utils"
"golang.org/x/crypto/ssh"
"go.chromium.org/chromiumos/config/go/test/api"
"google.golang.org/protobuf/types/known/anypb"
)
const (
// sshMsgIgnore is the SSH global message sent to ping the host.
// See RFC 4253 11.2, "Ignored Data Message".
sshMsgIgnore = "SSH_MSG_IGNORE"
pingTimeout = time.Second
pingRetryDelay = 2 * time.Second
pingRetryCount = 20
networkWaitTime = 1 * time.Minute
powerResetDelay = 10 * time.Second
networkErrMsg = "Network is unreachable"
cpuUartCapture = "cpu_uart_capture"
engImg = "-eng"
androidBuild = "android-build"
)
var launchTargetPatterns = []*regexp.Regexp{
regexp.MustCompile(`build_details/P?[0-9]+/([a-z_\-]+)`),
regexp.MustCompile(`artifacts_list/P?[0-9]+/([a-z_\-]+)`)}
var buildIdPatterns = []*regexp.Regexp{
regexp.MustCompile(`build_details/(P?[0-9]+)/`),
regexp.MustCompile(`artifacts_list/(P?[0-9]+)/`)}
// FullOSImageState copies the full Android/Cros image onto DUT disk.
type FullOSImageState struct {
params *CrossOverParameters
}
func NewFullOSImageState(params *CrossOverParameters) common_utils.ServiceState {
return &FullOSImageState{
params: params,
}
}
func (s FullOSImageState) Execute(ctx context.Context, log *log.Logger) (*anypb.Any, api.InstallResponse_Status, error) {
log.Println("Executing " + s.Name())
dutAddress := fmt.Sprintf("%s:%v", s.params.Dut.GetChromeos().GetSsh().GetAddress(), s.params.Dut.GetChromeos().GetSsh().GetPort())
var client *ssh.Client
if client = checkSSH(log, dutAddress, bootWaitRetryCount, bootWaitRetryInterval); client == nil {
setPDRole(ctx, log, "src", s.params)
errMsg := "cannot SSH onto DUT booted from USB common provision image"
return common_utils.WrapStringInAny(s.errStatus(errMsg)), api.InstallResponse_STATUS_PRE_PROVISION_SETUP_FAILED, errors.New(errMsg)
}
defer client.Close()
setPDRole(ctx, log, "src", s.params)
cacheServer := s.params.Dut.GetCacheServer().GetAddress()
ipFunc := func() {
log.Println("IP info")
var session *ssh.Session
session, err := client.NewSession()
if err != nil {
log.Println("Failed to create session: ", err)
return
}
// Jumble the command, we don't care what fails or not, any output, simply print it.
const ipCmd = "set -e; ip rule show; ip route show table all; route; ifconfig eth0; echo 'All commands succeeded'"
if output, err := session.CombinedOutput(ipCmd); err != nil {
log.Println("Warning: failed to run IP info commands: ", ipCmd, err)
log.Println(string(output))
} else {
log.Println(string(output))
}
}
networkWaitFunc := func() error {
shorterCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
networkCheckCmd := fmt.Sprintf("curl --max-time 5 -s -o/dev/null http://%s:%v/check_health", cacheServer.GetAddress(), cacheServer.GetPort())
for {
var session *ssh.Session
session, err := client.NewSession()
if err != nil {
log.Println("Failed to create session: ", err)
return err
}
select {
case <-shorterCtx.Done():
return errors.New("Failed while waiting for network connection.")
default:
if err := session.Run(networkCheckCmd); err != nil {
ipFunc()
// Retry w/ delay.
time.Sleep(time.Second)
continue
}
return nil
}
}
}
if err := networkWaitFunc(); err != nil {
log.Println(err)
}
installCommand := ""
targetImgPath := s.params.TargetImagePath.GetPath()
if strings.Contains(targetImgPath, androidBuild) {
fwBuildPath := ""
if partnerGCSBucket := s.params.PartnerMetadata.GetPartnerGcsBucket(); partnerGCSBucket != "" {
// Set the Firmware blob path for the given model (b/379953281)
fwBuildPath = fmt.Sprintf("%s/provision_images/al-fw-blobs", partnerGCSBucket)
}
buildId, err := targetBuild(targetImgPath)
if err != nil {
return common_utils.WrapStringInAny(s.errStatus("INFRA: unable to parse image path from target")), api.InstallResponse_STATUS_INVALID_REQUEST, err
}
launchTarget, err := extractTargetLaunch(targetImgPath)
if err != nil {
return common_utils.WrapStringInAny(s.errStatus("INFRA: unable to parse image path from target")), api.InstallResponse_STATUS_INVALID_REQUEST, err
}
if strings.Contains(launchTarget, engImg) {
// Uart logs are only enabled in -eng img.
callServodSET(ctx, log, cpuUartCapture, on, s.params)
}
installCommand = fmt.Sprintf("al-install android-build/builds/%s/%s/attempts/latest/artifacts/android-desktop_image.bin.gz %s %s", buildId, launchTarget, cacheServer.GetAddress(), fwBuildPath)
} else {
installCommand = fmt.Sprintf("cros-install %s %s", targetImgPath[5:], cacheServer.GetAddress())
}
if err := installCommandWithRetry(client, installCommand, log); err != nil {
log.Println("Failed to run installCommand: ", installCommand, err)
collectUARTLogs(ctx, log, s.params)
return common_utils.WrapStringInAny(s.errStatus("FAILED TO RUN CROSOVER INSTALL COMMAND")), api.InstallResponse_STATUS_DOWNLOADING_IMAGE_FAILED, err
}
errStatus, responseStatus, err := s.handleReboot(ctx, client, log)
if err != nil {
collectUARTLogs(ctx, log, s.params)
}
return errStatus, responseStatus, err
}
func (s FullOSImageState) errStatus(curErr string) string {
if s.params.PrevError != "" {
return fmt.Sprintf("Flash and OTA failed: %s", s.params.PrevError)
}
return curErr
}
func (s FullOSImageState) Next() common_utils.ServiceState {
return NewValidateState(s.params)
}
func (s FullOSImageState) Name() string {
return "Full OS Image State"
}
func (s FullOSImageState) handleReboot(ctx context.Context, sshClient *ssh.Client, log *log.Logger) (*anypb.Any, api.InstallResponse_Status, error) {
if err := callServodRetry(ctx, log, "power_state", "off", s.params); err != nil {
return common_utils.WrapStringInAny("INFRA: unable to set turn off power via servo"), api.InstallResponse_STATUS_PROVISIONING_FAILED, err
}
if err := sshFailWait(sshClient, log); err != nil {
return common_utils.WrapStringInAny("INFRA: dut did not power off post provision image installation"), api.InstallResponse_STATUS_PROVISIONING_FAILED, err
}
log.Println("Device successfully powered off.")
if err := callServodRetry(ctx, log, "image_usbkey_direction", "servo_sees_usbkey", s.params); err != nil {
return common_utils.WrapStringInAny("INFRA: unable to set usb direction via servo"), api.InstallResponse_STATUS_PROVISIONING_FAILED, err
}
log.Printf("\nWaiting for the image_usbkey_direction.")
// TODO: Instead of fixed wait time, use a poll based status checker. Refer WaitForPowerStates and GetECSystemPowerState in firmware code.
time.Sleep(10 * time.Second)
if err := callServodRetry(ctx, log, "power_state", "on", s.params); err != nil {
// reset might be needed if power on failed: b/381982169
log.Println("Warning: unable to set turn on power via servo ", err)
log.Println("Waiting for 10 seconds before trying power resetting.")
time.Sleep(powerResetDelay)
if err := callServodRetry(ctx, log, "power_state", "reset", s.params); err != nil {
return common_utils.WrapStringInAny("INFRA: unable to reset power via servo"), api.InstallResponse_STATUS_PROVISIONING_FAILED, err
}
}
return nil, api.InstallResponse_STATUS_SUCCESS, nil
}
func installCommandWithRetry(client *ssh.Client, installCommand string, log *log.Logger) error {
log.Println("Running command on host now ", installCommand)
retryCount := 3
for ; retryCount >= 0; retryCount-- {
var session *ssh.Session
session, err := client.NewSession()
if err != nil {
log.Println("Failed to create session: ", err)
return err
}
output, err := session.CombinedOutput(installCommand)
log.Println(string(output))
if err == nil {
return nil
}
if !strings.Contains(string(output), networkErrMsg) {
return err
}
log.Println("Got error " + networkErrMsg + " while running install command. Will Retry...")
}
return fmt.Errorf("Failed to run installCommand")
}
// sshFailWait return nil, if the device cannot be sshed within 40 seconds.
// If the device is alive even after 40 seconds, it returns error.
func sshFailWait(sshClient *ssh.Client, log *log.Logger) error {
for attemp := 1; attemp < pingRetryCount; attemp++ {
log.Println("pinging device to check if its offline...")
if err := ping(sshClient, pingTimeout); err != nil {
return nil
}
time.Sleep(pingRetryDelay)
}
return errors.New("dut failed to become unreachable")
}
func ping(sshClient *ssh.Client, timeout time.Duration) error {
ch := make(chan error, 1)
go func() {
_, _, err := sshClient.SendRequest(sshMsgIgnore, true, []byte{})
ch <- err
}()
select {
case err := <-ch:
return err
case <-time.After(timeout):
return errors.New("timed out")
}
}
func extractTargetLaunch(path string) (string, error) {
for _, re := range launchTargetPatterns {
matches := re.FindStringSubmatch(path)
if len(matches) == 2 {
return matches[1], nil
}
}
return "", fmt.Errorf("could not extract launch from %s", path)
}
func targetBuild(imagePath string) (string, error) {
for _, re := range buildIdPatterns {
matches := re.FindStringSubmatch(imagePath)
if len(matches) == 2 {
return matches[1], nil
}
}
return "", fmt.Errorf("could not extract buildId from %s", imagePath)
}