blob: 9c4a3b659009631eec7bcc1fc84e05ab941c95e5 [file] [log] [blame]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Service interfaces bases
package services
import (
"context"
"fmt"
"time"
"go.chromium.org/chromiumos/config/go/longrunning"
api "go.chromium.org/chromiumos/config/go/test/api"
lab_api "go.chromium.org/chromiumos/config/go/test/lab/api"
)
// ServiceAdapters are used to interface with a DUT
// All methods here are proxies to cros-dut (with some additions for simplicity)
type ServiceAdapterInterface interface {
// RunCmd takes a command and argument and executes it remotely in the DUT,
// returning the stdout as the string result and any execution error as the error.
RunCmd(ctx context.Context, cmd string, args []string) (string, error)
// Restart restarts a DUT (allowing cros-dut to reconnect for connection caching).
Restart(ctx context.Context) error
// PathExists is a simple wrapper for RunCmd for the sake of simplicity. If
// the path exists True is returned, else False. An error implies a
// a communication failure.
PathExists(ctx context.Context, path string) (bool, error)
// PipeData uses the caching infrastructure to bring an image into the lab.
// Contrary to CopyData, the data here is pipeable to whatever is fed into
// pipeCommand, rather than directly placed locally.
PipeData(ctx context.Context, sourceUrl string, pipeCommand string) error
// CopyData uses the caching infrastructure to copy a remote image to
// the local path specified by destPath.
CopyData(ctx context.Context, sourceUrl string, destPath string) error
// DeleteDirectory is a simple wrapper for RunCmd for the sake of simplicity.
DeleteDirectory(ctx context.Context, dir string) error
// CreateDirectory is a simple wrapper for RunCmd for the sake of simplicity.
// All directories specified in the array will be created.
// As this uses "-p" option, subdirs are created regardless of whether parents
// exist or not.
CreateDirectories(ctx context.Context, dirs []string) error
// ForceReconnectWithBackoff waits for the DUT to come back up/reconnect.
ForceReconnectWithBackoff(ctx context.Context) error
}
type ServiceAdapter struct {
dut *lab_api.Dut
dutClient api.DutServiceClient
noReboot bool
}
func NewServiceAdapter(dut *lab_api.Dut, dutClient api.DutServiceClient, noReboot bool) ServiceAdapter {
return ServiceAdapter{
dut: dut,
dutClient: dutClient,
noReboot: noReboot,
}
}
// RunCmd runs a command in a remote DUT
func (s ServiceAdapter) RunCmd(ctx context.Context, cmd string, args []string) (string, error) {
fmt.Printf("Run cmd: %s, %s\n", cmd, args)
req := api.ExecCommandRequest{
Command: cmd,
Args: args,
Stdout: api.Output_OUTPUT_PIPE,
Stderr: api.Output_OUTPUT_PIPE,
}
stream, err := s.dutClient.ExecCommand(ctx, &req)
if err != nil {
return "", fmt.Errorf("execution fail: %w", err)
}
// Expecting single stream result
execCmdResponse, err := stream.Recv()
if err != nil {
return "", fmt.Errorf("execution single stream result: %w", err)
}
fmt.Printf("Run cmd response: %s\n", execCmdResponse)
if execCmdResponse.ExitInfo.Status != 0 {
err = fmt.Errorf("status:%v message:%v", execCmdResponse.ExitInfo.Status, execCmdResponse.ExitInfo.ErrorMessage)
}
if string(execCmdResponse.Stderr) != "" {
fmt.Printf("execution finished with stderr: %s\n", string(execCmdResponse.Stderr))
}
return string(execCmdResponse.Stdout), err
}
// Restart restarts a DUT
func (s ServiceAdapter) Restart(ctx context.Context) error {
if s.noReboot {
return nil
}
req := api.RestartRequest{
Args: []string{},
}
ctx, cancel := context.WithTimeout(ctx, 300*time.Second)
defer cancel()
op, err := s.dutClient.Restart(ctx, &req)
if err != nil {
return err
}
for !op.Done {
time.Sleep(1 * time.Second)
}
switch x := op.Result.(type) {
case *longrunning.Operation_Error:
return fmt.Errorf(x.Error.Message)
case *longrunning.Operation_Response:
return nil
}
return nil
}
// PathExists determines if a path exists in a DUT
func (s ServiceAdapter) PathExists(ctx context.Context, path string) (bool, error) {
exists, err := s.RunCmd(ctx, "", []string{"[", "-e", path, "]", "&&", "echo", "-n", "1", "||", "echo", "-n", "0"})
if err != nil {
return false, fmt.Errorf("path exists: failed to check if %s exists, %s", path, err)
}
return exists == "1", nil
}
// PipeData uses the caching infrastructure to bring a file locally,
// allowing a user to pipe the result to any desired application.
func (s ServiceAdapter) PipeData(ctx context.Context, sourceUrl string, pipeCommand string) error {
fmt.Printf("Piping %s with command %s\n", sourceUrl, pipeCommand)
req := api.CacheRequest{
Source: &api.CacheRequest_GsFile{
GsFile: &api.CacheRequest_GSFile{
SourcePath: sourceUrl,
},
},
Destination: &api.CacheRequest_Pipe_{
Pipe: &api.CacheRequest_Pipe{
Commands: pipeCommand,
},
},
}
op, err := s.dutClient.Cache(ctx, &req)
if err != nil {
return fmt.Errorf("execution failure: %v", err)
}
for !op.Done {
time.Sleep(1 * time.Second)
}
switch x := op.Result.(type) {
case *longrunning.Operation_Error:
return fmt.Errorf(x.Error.Message)
case *longrunning.Operation_Response:
return nil
}
return nil
}
// CopyData caches a file for a DUT locally from a GS url.
func (s ServiceAdapter) CopyData(ctx context.Context, sourceUrl string, destPath string) error {
fmt.Printf("Copy data from: %s, to: %s\n", sourceUrl, destPath)
req := api.CacheRequest{
Source: &api.CacheRequest_GsFile{
GsFile: &api.CacheRequest_GSFile{
SourcePath: sourceUrl,
},
},
Destination: &api.CacheRequest_File{
File: &api.CacheRequest_LocalFile{
Path: destPath,
},
},
}
op, err := s.dutClient.Cache(ctx, &req)
if err != nil {
return fmt.Errorf("execution failure: %v", err)
}
for !op.Done {
time.Sleep(1 * time.Second)
}
switch x := op.Result.(type) {
case *longrunning.Operation_Error:
return fmt.Errorf(x.Error.Message)
case *longrunning.Operation_Response:
return nil
}
return nil
}
// DeleteDirectory is a thin wrapper around an rm command. Done here as it is
// expected to be reused often by many services.
func (s ServiceAdapter) DeleteDirectory(ctx context.Context, dir string) error {
if _, err := s.RunCmd(ctx, "rm", []string{"-rf", dir}); err != nil {
return fmt.Errorf("could not delete directory, %w", err)
}
return nil
}
// Create directories is a thin wrapper around an mkdir command. Done here as it
// is expected to be reused often by many services.
func (s ServiceAdapter) CreateDirectories(ctx context.Context, dirs []string) error {
if _, err := s.RunCmd(ctx, "mkdir", append([]string{"-p"}, dirs...)); err != nil {
return fmt.Errorf("could not create directory, %w", err)
}
return nil
}
// ForceReconnectWithBackoff is a thin wrapper around DUT service's ForceReconnectWithBackoff.
func (s ServiceAdapter) ForceReconnectWithBackoff(ctx context.Context) error {
fmt.Printf("ForceReconnectWithBackoff Start")
ctx, cancel := context.WithTimeout(ctx, 500*time.Second)
defer cancel()
expDelay := 1
loop:
for {
select {
case <-ctx.Done():
return fmt.Errorf("could not force reconnect with backoff")
default:
f := func() error {
op, err := s.dutClient.ForceReconnect(ctx, &api.ForceReconnectRequest{})
if err != nil {
fmt.Printf("ForceReconnectWithBackoff ERROR %s", err)
return err
}
for !op.Done {
fmt.Printf("ForceReconnectWithBackoff !op.Done")
time.Sleep(1 * time.Second)
}
switch x := op.Result.(type) {
case *longrunning.Operation_Error:
fmt.Printf("ForceReconnectWithBackoff ignoring LRO ERROR %s", x.Error.Message)
return fmt.Errorf(x.Error.Message)
case *longrunning.Operation_Response:
fmt.Printf("ForceReconnectWithBackoff LRO RESPONSE")
return nil
default:
return fmt.Errorf("unknown longrunniong operation")
}
}
if err := f(); err == nil {
// Break out the loop now.
break loop
}
}
delay := expDelay
if delay < 16 {
expDelay = expDelay * 2
}
time.Sleep(time.Duration(delay) * time.Second)
}
fmt.Printf("ForceReconnectWithBackoff Success")
return nil
}