blob: b3f53e7c172c28febb3b5cfa556748de51878c98 [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.
// Package servodserver implements servod_service.proto (see proto for details)
package servodserver
import (
"bytes"
"context"
"errors"
"io"
"log"
"strings"
"go.chromium.org/chromiumos/lro"
"go.chromium.org/chromiumos/test/servod/cmd/commandexecutor"
"go.chromium.org/chromiumos/test/servod/cmd/model"
"go.chromium.org/chromiumos/test/servod/cmd/servod"
"go.chromium.org/chromiumos/test/servod/cmd/ssh"
"go.chromium.org/chromiumos/config/go/longrunning"
"go.chromium.org/chromiumos/config/go/test/api"
crypto_ssh "golang.org/x/crypto/ssh"
)
// ServodService implementation of servod_service.proto
type ServodService struct {
manager *lro.Manager
logger *log.Logger
commandexecutor commandexecutor.CommandExecutorInterface
sshPool *ssh.Pool
servodPool *servod.Pool
}
// NewServodService creates a new servod service.
func NewServodService(ctx context.Context, logger *log.Logger, commandexecutor commandexecutor.CommandExecutorInterface) (*ServodService, func(), error) {
config, err := ssh.NewDefaultConfig()
if err != nil {
return nil, nil, err
}
if isCloudBot() {
if err = config.Load(defaultSSHConfigPathOnCloudBot); err != nil {
return nil, nil, err
}
}
servodService := &ServodService{
manager: lro.New(),
logger: logger,
commandexecutor: commandexecutor,
sshPool: ssh.New(config),
servodPool: servod.NewPool(),
}
destructor := func() {
servodService.manager.Close()
}
return servodService, destructor, nil
}
// StartServod runs a servod Docker container and starts the servod daemon
// inside the container if servod is containerized. Otherwise, it simply
// starts the servod daemon.
func (s *ServodService) StartServod(ctx context.Context, req *api.StartServodRequest) (*longrunning.Operation, error) {
s.logger.Printf("Received api.StartServodRequest: %#v\n", req)
op := s.manager.NewOperation()
a := model.CliArgs{
ServoHostPath: req.ServoHostPath,
ServodDockerContainerName: req.ServodDockerContainerName,
ServodDockerImagePath: req.ServodDockerImagePath,
ServodPort: req.ServodPort,
Board: req.Board,
Model: req.Model,
SerialName: req.SerialName,
Debug: req.Debug,
RecoveryMode: req.RecoveryMode,
Config: req.Config,
AllowDualV4: req.AllowDualV4,
}
_, bErr, err := s.RunCli(model.CliStartServod, a, nil, false)
if err != nil {
s.logger.Println("Failed to run CLI: ", err)
s.manager.SetResult(op.Name, &api.StartServodResponse{
Result: &api.StartServodResponse_Failure_{
Failure: &api.StartServodResponse_Failure{
ErrorMessage: getErrorMessage(bErr, err),
},
},
})
} else {
s.manager.SetResult(op.Name, &api.StartServodResponse{
Result: &api.StartServodResponse_Success_{},
})
}
return op, err
}
// StopServod stops the servod daemon inside the container and stops the
// servod Docker container if servod is containerized. Otherwise, it simply
// stops the servod daemon.
func (s *ServodService) StopServod(ctx context.Context, req *api.StopServodRequest) (*longrunning.Operation, error) {
s.logger.Printf("Received api.StopServodRequest: %#v\n", req)
op := s.manager.NewOperation()
a := model.CliArgs{
ServoHostPath: req.ServoHostPath,
ServodDockerContainerName: req.ServodDockerContainerName,
ServodPort: req.ServodPort,
}
_, bErr, err := s.RunCli(model.CliStopServod, a, nil, false)
if err != nil {
s.logger.Println("Failed to run CLI: ", err)
s.manager.SetResult(op.Name, &api.StopServodResponse{
Result: &api.StopServodResponse_Failure_{
Failure: &api.StopServodResponse_Failure{
ErrorMessage: getErrorMessage(bErr, err),
},
},
})
} else {
s.manager.SetResult(op.Name, &api.StopServodResponse{
Result: &api.StopServodResponse_Success_{},
})
}
return op, err
}
// ExecCmd executes a system command that is provided through the command
// parameter in the request. It allows the user to execute arbitrary commands
// that can't be handled by calling servod (e.g. update firmware through
// "futility", remote file copy through "scp").
// It executes the command inside the servod Docker container if the
// servod_docker_container_name parameter is provided in the request.
// Otherwise, it executes the command directly inside the host that the servo
// is physically connected to.
func (s *ServodService) ExecCmd(ctx context.Context, req *api.ExecCmdRequest) (*api.ExecCmdResponse, error) {
s.logger.Printf("Received api.ExecCmdRequest: %#v\n", req)
a := model.CliArgs{
ServoHostPath: req.ServoHostPath,
ServodDockerContainerName: req.ServodDockerContainerName,
Command: req.Command,
}
var stdin io.Reader = nil
if len(req.Stdin) > 0 {
stdin = bytes.NewReader(req.Stdin)
}
bOut, bErr, err := s.RunCli(model.CliExecCmd, a, stdin, false)
if err != nil {
s.logger.Println("Failed to run CLI: ", err)
}
return &api.ExecCmdResponse{
ExitInfo: getExitInfo(err),
Stdout: bOut.Bytes(),
Stderr: bErr.Bytes(),
}, err
}
// CallServod runs a servod command through an XML-RPC call.
// It runs the command inside the servod Docker container if the
// servod_docker_container_name parameter is provided in the request.
// Otherwise, it runs the command directly inside the host that the servo
// is physically connected to.
// Allowed methods: doc, get, set, and hwinit.
func (s *ServodService) CallServod(ctx context.Context, req *api.CallServodRequest) (*api.CallServodResponse, error) {
s.logger.Printf("Received api.CallServodRequest: %#v\n", req)
sd, err := s.servodPool.Get(
req.ServoHostPath,
req.ServodPort,
// This method must return non-nil value for servod.Get to work so return a dummy array.
func() ([]string, error) {
return []string{}, nil
})
if err != nil {
return &api.CallServodResponse{
Result: &api.CallServodResponse_Failure_{
Failure: &api.CallServodResponse_Failure{
ErrorMessage: err.Error(),
},
},
}, err
}
val, err := sd.Call(ctx, s.sshPool, strings.ToLower(req.Method.String()), req.Args)
if err != nil {
return &api.CallServodResponse{
Result: &api.CallServodResponse_Failure_{
Failure: &api.CallServodResponse_Failure{
ErrorMessage: err.Error(),
},
},
}, err
}
return &api.CallServodResponse{
Result: &api.CallServodResponse_Success_{
Success: &api.CallServodResponse_Success{
Result: val,
},
},
}, nil
}
// getErrorMessage returns either Stderr output or error message
func getErrorMessage(bErr bytes.Buffer, err error) string {
errorMessage := bErr.String()
if errorMessage == "" {
errorMessage = err.Error()
}
return errorMessage
}
// getExitInfo extracts exit info from Session Run's error
func getExitInfo(runError error) *api.ExecCmdResponse_ExitInfo {
// If no error, command succeeded
if runError == nil {
return createCommandSucceededExitInfo()
}
// If ExitError, command ran but did not succeed
var ee *crypto_ssh.ExitError
if errors.As(runError, &ee) {
return createCommandFailedExitInfo(ee)
}
// Otherwise we assume command failed to start
return createFailedToStartExitInfo(runError)
}
func createFailedToStartExitInfo(err error) *api.ExecCmdResponse_ExitInfo {
return &api.ExecCmdResponse_ExitInfo{
Status: 42, // Contract dictates arbitrary response, thus 42 is as good as any number
Signaled: false,
Started: false,
ErrorMessage: err.Error(),
}
}
func createCommandSucceededExitInfo() *api.ExecCmdResponse_ExitInfo {
return &api.ExecCmdResponse_ExitInfo{
Status: 0,
Signaled: false,
Started: true,
ErrorMessage: "",
}
}
func createCommandFailedExitInfo(err *crypto_ssh.ExitError) *api.ExecCmdResponse_ExitInfo {
return &api.ExecCmdResponse_ExitInfo{
Status: int32(err.ExitStatus()),
Signaled: true,
Started: true,
ErrorMessage: "",
}
}