Added CallServod to support XMLRPC calls.
Change-Id: I3e8d208066bbdd59751854e1e30131d52f3c4788
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/3371987
Auto-Submit: Deniz Kara <denizkara@google.com>
Reviewed-by: Jaques Clapauch <jaquesc@google.com>
Tested-by: Deniz Kara <denizkara@google.com>
Commit-Queue: Deniz Kara <denizkara@google.com>
diff --git a/src/chromiumos/test/servod/cmd/commandexecutor/servodcommandexecutor.go b/src/chromiumos/test/servod/cmd/commandexecutor/servodcommandexecutor.go
index 69bd94b..69ab150 100644
--- a/src/chromiumos/test/servod/cmd/commandexecutor/servodcommandexecutor.go
+++ b/src/chromiumos/test/servod/cmd/commandexecutor/servodcommandexecutor.go
@@ -15,6 +15,9 @@
"golang.org/x/crypto/ssh"
)
+// ServodCommandExecutor acts as a receiver to implement CommandExecutorInterface
+// by running given commands either locally or on a remote host through os/exec
+// and SSH run commands.
type ServodCommandExecutor struct {
logger *log.Logger
}
diff --git a/src/chromiumos/test/servod/cmd/cros-servod/main.go b/src/chromiumos/test/servod/cmd/cros-servod/main.go
index a573164..b3f7468 100644
--- a/src/chromiumos/test/servod/cmd/cros-servod/main.go
+++ b/src/chromiumos/test/servod/cmd/cros-servod/main.go
@@ -39,28 +39,48 @@
Usage:
To start servod:
cros-servod cli start_servod --servo_host_path <ip:port> --servod_docker_container_name <a_unique_name> --servod_docker_image_path <servod_docker_image_path> --servod_port <servod_port> --board <board> --model <model> --serial_name <serial_name> --debug <debug> --recovery_mode <recovery_mode> --config <config> --allow_dual_v4 <allow_dual_v4> [--log_path /tmp/servod/]
+ Example (Start non-containerized servod):
+ cros-servod cli start_servod --servo_host_path localhost:9876 --servod_port 9901 --board dedede --model galith --serial_name G1911050826
+ Example (Start containerized servod):
+ cros-servod cli start_servod --servo_host_path localhost:9876 --servod_docker_container_name servod_container1 --servod_docker_image_path gcr.io/chromeos-bot/servod@sha256:2d25f6313c7bbac349607 --servod_port 9901 --board dedede --model galith --serial_name G1911050826
To stop servod:
cros-servod cli stop_servod --servo_host_path <ip:port> --servod_docker_container_name <a_unique_name> --servod_port <servod_port> [--log_path /tmp/servod/]
+ Example (Stop non-containerized servod):
+ cros-servod cli stop_servod --servo_host_path localhost:9876 --servod_port 9901
+ Example (Stop containerized servod):
+ cros-servod cli stop_servod --servo_host_path localhost:9876 --servod_docker_container_name servod_container1 --servod_port 9901
To execute command:
cros-servod cli exec_cmd --servo_host_path <ip:port> --servod_docker_container_name <a_unique_name> --command <command> [--log_path /tmp/servod/]
+ Example (Execute fake disconnect command on non-containerized servod):
+ cros-servod cli exec_cmd --servo_host_path localhost:9876 --command "dut-control -p 9901 servo_v4_uart_cmd:'fakedisconnect 130 2400'"
+ Example (Execute system command on containerized servod):
+ cros-servod cli exec_cmd --servo_host_path localhost:9876 --servod_docker_container_name servod_container1 --command "ps -ef | grep servod"
To call servod for doc:
- cros-servod cli call_servod --servo_host_path <ip:port> --servod_docker_container_name <a_unique_name> --method doc [--log_path /tmp/servod/]
+ cros-servod cli call_servod --servo_host_path <ip:port> --servod_docker_container_name <a_unique_name> --method doc --args <args> [--log_path /tmp/servod/]
+ Example (Call DOC method for lid_open control on non-containerized servod):
+ cros-servod cli call_servod --servo_host_path localhost:9876 --method doc --args lid_open
To call servod for get:
cros-servod cli call_servod --servo_host_path <ip:port> --servod_docker_container_name <a_unique_name> --method get --args <args> [--log_path /tmp/servod/]
+ Example (Call GET method for lid_open control on non-containerized servod):
+ cros-servod cli call_servod --servo_host_path localhost:9876 --method get --args lid_open
To call servod for set:
cros-servod cli call_servod --servo_host_path <ip:port> --servod_docker_container_name <a_unique_name> --method set --args <args> [--log_path /tmp/servod/]
+ Example (Call SET method to set lid_open control value to yes on containerized servod):
+ cros-servod cli call_servod --servo_host_path localhost:9876 --servod_docker_container_name servod_container1 --method set --args "lid_open:yes"
+ Example (Call SET method to execute fake disconnect on non-containerized servod):
+ cros-servod cli call_servod --servo_host_path localhost:9876 --method set --args "servo_v4_uart_cmd:'fakedisconnect 130 2400'"
server Starts the servod server for RPC calls. Mostly used for tests.
Usage:
cros-servod server [--log_path /tmp/servod/] [--server_port 80]
--version Prints the version.
-
+
--help Prints the help.`
defaultLogDirectory = "/tmp/servod/"
defaultServerPort = 80
diff --git a/src/chromiumos/test/servod/cmd/model/model.go b/src/chromiumos/test/servod/cmd/model/model.go
index bf5e764..44b80e2 100644
--- a/src/chromiumos/test/servod/cmd/model/model.go
+++ b/src/chromiumos/test/servod/cmd/model/model.go
@@ -2,14 +2,22 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+// The model package holds the data model that is common to multiple packages
+// in the project. For example, CliArgs is used in both main and servodserver
+// packages.
package model
+// CliArgs is a data structure that holds the CLI arguments passed.
type CliArgs struct {
// Common input params.
// Local log file path.
LogPath string
// The path (URI) for the servod (containerized or running as a daemon) host.
+ // It can be in IP:PORT format or an absolute path.
+ // For local testing, you can do port forwarding as follows:
+ // ssh -L 9876:localhost:22 root@SERVO_HOST_IP
+ // Then, you can use localhost:9876 as ServoHostPath value.
// If cros-servod and docker-servod live on the same host, this parameter
// should be empty.
ServoHostPath string
@@ -52,11 +60,13 @@
Method string
// The arguments to pass to the method. For the doc and get methods,
- // there will be a single argument, which is the control name (e.g.
- // cli --method get --args fakedisconnect). For the set method,
- // it will be the control name and the value separated with a colon
- // and wrapped inside a quote (e.g.
- // cli --method set --args “fakedisconnect:100 2000”).
+ // there will be a single argument which is the control name
+ // (e.g. cli --method get --args lid_open). For the set method, it will
+ // be the control name and the value separated with a colon and wrapped
+ // inside a quote (e.g. cli --method set --args "lid_open:yes"). If the
+ // control value for the set operation includes non-alphanumeric characters
+ // such as space, it should be wrapped with a single quote (e.g.
+ // cli --method set --args "servo_v4_uart_cmd:'fakedisconnect 100 2000'").
Args string
// The port for the servod GRPC server.
@@ -67,9 +77,18 @@
type CliSubcommand string
const (
- CliUnknown CliSubcommand = ""
+ // This is used when the command value passed in not known by the app.
+ CliUnknown CliSubcommand = ""
+
+ // CLI subcommand to start servod.
CliStartServod CliSubcommand = "start_servod"
- CliStopServod CliSubcommand = "stop_servod"
- CliExecCmd CliSubcommand = "exec_cmd"
- CliCallServod CliSubcommand = "call_servod"
+
+ // CLI subcommand to stop servod.
+ CliStopServod CliSubcommand = "stop_servod"
+
+ // CLI subcommand to execute a servod command.
+ CliExecCmd CliSubcommand = "exec_cmd"
+
+ // CLI subcommand to call servod (DOC, GET, and SET).
+ CliCallServod CliSubcommand = "call_servod"
)
diff --git a/src/chromiumos/test/servod/cmd/servod/pool.go b/src/chromiumos/test/servod/cmd/servod/pool.go
new file mode 100644
index 0000000..2326c6a
--- /dev/null
+++ b/src/chromiumos/test/servod/cmd/servod/pool.go
@@ -0,0 +1,86 @@
+// Copyright 2021 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package servod
+
+import (
+ "fmt"
+
+ "go.chromium.org/luci/common/errors"
+)
+
+// Pool is a pool of servod to reuse.
+//
+// Servo are pooled by the `address:port|remote` they are connected to.
+//
+// Users should call Get, which returns a instance from the pool if available,
+// or creates and returns a new one.
+// The returned servod is not guaranteed to be good,
+// e.g., the connection may have broken while the Client was in the pool.
+//
+// The user should not close the servod as Pool will close it at the end.
+//
+// The user should Close the pool after use, to free any resources in the pool.
+type Pool struct {
+ servos map[string]*servod
+}
+
+// NewPool returns a new Pool. The provided ssh config is used for new SSH
+// connections if pool has none to reuse.
+func NewPool() *Pool {
+ return &Pool{
+ servos: make(map[string]*servod),
+ }
+}
+
+// Close closes all active servodes.
+func (p *Pool) Close() error {
+ for k, s := range p.servos {
+ if err := s.Close(); err != nil {
+ return errors.Annotate(err, "close pool").Err()
+ }
+ delete(p.servos, k)
+ }
+ return nil
+}
+
+// getServoParams function to receive start params for servod.
+type getServoParams func() ([]string, error)
+
+// Get provides servod from cache or initiate new one.
+func (p *Pool) Get(servoAddr string, servodPort int32, getParams getServoParams) (*servod, error) {
+ if s, ok := p.servos[createKey(servoAddr, servodPort)]; ok {
+ return s, nil
+ }
+ s, err := p.init(servoAddr, servodPort, getParams)
+ if err != nil {
+ return nil, errors.Annotate(err, "get from pool").Err()
+ }
+ return s, nil
+}
+
+// init creates new servod instance and places it in the cache.
+func (p *Pool) init(servoAddr string, servodPort int32, getParams getServoParams) (*servod, error) {
+ if getParams == nil {
+ return nil, errors.Reason("init servod: getParams is not provided").Err()
+ }
+ if servoAddr == "" {
+ return nil, errors.Reason("init servod: servoAddr is empty").Err()
+ }
+ if servodPort > 9999 || servodPort < 1 {
+ return nil, errors.Reason("init servod: servodPort expected to in range 1-9999").Err()
+ }
+ s := &servod{
+ host: servoAddr,
+ port: servodPort,
+ getParams: getParams,
+ }
+ p.servos[createKey(servoAddr, servodPort)] = s
+ return s, nil
+}
+
+// createKey creates key for the pool.
+func createKey(servoAddr string, servodPort int32) string {
+ return fmt.Sprintf("%s|%d", servoAddr, servodPort)
+}
diff --git a/src/chromiumos/test/servod/cmd/servod/proxy.go b/src/chromiumos/test/servod/cmd/servod/proxy.go
new file mode 100644
index 0000000..d844b35
--- /dev/null
+++ b/src/chromiumos/test/servod/cmd/servod/proxy.go
@@ -0,0 +1,134 @@
+// Copyright 2021 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package servod
+
+import (
+ "fmt"
+ "io"
+ "net"
+ "sync"
+
+ "go.chromium.org/luci/common/errors"
+
+ "infra/libs/sshpool"
+)
+
+// proxy holds info to perform proxy confection to servod daemon.
+type proxy struct {
+ host string
+ connFunc func() (net.Conn, error)
+ ls net.Listener
+ mutex sync.Mutex
+ errFuncs []func(error)
+ closed bool
+}
+
+const (
+ // Local address with dynamic port.
+ localAddr = "127.0.0.1:0"
+ // Local address template for remote host.
+ remoteAddrFmt = "127.0.0.1:%d"
+)
+
+// newProxy creates a new proxy with forward from remote to local host.
+// Function is using a goroutine to listen and handle each incoming connection.
+// Initialization of proxy is going asynchronous after return proxy instance.
+func newProxy(pool *sshpool.Pool, host string, remotePort int32, errFuncs ...func(error)) (*proxy, error) {
+ remoteAddr := fmt.Sprintf(remoteAddrFmt, remotePort)
+ connFunc := func() (net.Conn, error) {
+ conn, err := pool.Get(host)
+ if err != nil {
+ return nil, errors.Annotate(err, "get proxy %q: fail to get client from pool", host).Err()
+ }
+ defer func() { pool.Put(host, conn) }()
+ // Establish connection with remote server.
+ return conn.Dial("tcp", remoteAddr)
+ }
+ // Create listener for local port.
+ local, err := net.Listen("tcp", localAddr)
+ if err != nil {
+ return nil, err
+ }
+ proxy := &proxy{
+ host: host,
+ ls: local,
+ connFunc: connFunc,
+ errFuncs: errFuncs,
+ closed: false,
+ }
+ // Start a goroutine that serves as the listener and launches
+ // a new goroutine to handle each incoming connection.
+ // Running by goroutine to avoid waiting connections and return proxy for usage.
+ go func() {
+ for {
+ if proxy.closed {
+ break
+ }
+ // Waits for and returns the next connection.
+ local, err := proxy.ls.Accept()
+ if err != nil {
+ break
+ }
+ go func() {
+ if err := proxy.handleConn(local); err != nil && len(proxy.errFuncs) > 0 {
+ proxy.mutex.Lock()
+ for _, ef := range proxy.errFuncs {
+ ef(err)
+ }
+ proxy.mutex.Unlock()
+ }
+ }()
+ }
+ }()
+ return proxy, nil
+}
+
+// Close closes listening for incoming connections of proxy.
+func (p *proxy) Close() error {
+ p.closed = true
+ p.mutex.Lock()
+ p.errFuncs = nil
+ p.mutex.Unlock()
+ return p.ls.Close()
+}
+
+// handleConn establishes a new connection to the destination port using connFunc
+// and copies data between it and src. It closes src before returning.
+func (p *proxy) handleConn(src net.Conn) error {
+ if p.closed {
+ return errors.Reason("handle connection: proxy closed").Err()
+ }
+ defer func() { src.Close() }()
+
+ dst, err := p.connFunc()
+ if err != nil {
+ return err
+ }
+ defer func() { dst.Close() }()
+
+ ch := make(chan error)
+ go func() {
+ _, err := io.Copy(src, dst)
+ ch <- err
+ }()
+ go func() {
+ _, err := io.Copy(dst, src)
+ ch <- err
+ }()
+
+ var firstErr error
+ for i := 0; i < 2; i++ {
+ if err := <-ch; err != io.EOF && firstErr == nil {
+ firstErr = err
+ }
+ }
+ return firstErr
+}
+
+// LocalAddr provides assigned local address.
+// Example: 127.0.0.1:23456
+func (p *proxy) LocalAddr() string {
+ return p.ls.Addr().String()
+}
diff --git a/src/chromiumos/test/servod/cmd/servod/servod.go b/src/chromiumos/test/servod/cmd/servod/servod.go
new file mode 100644
index 0000000..0f44a8c
--- /dev/null
+++ b/src/chromiumos/test/servod/cmd/servod/servod.go
@@ -0,0 +1,170 @@
+// Copyright 2021 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Package servod provides functions to manage connection and communication with servod daemon on servo-host.
+package servod
+
+import (
+ "chromiumos/test/servod/cmd/ssh"
+ "chromiumos/test/servod/cmd/xmlrpc"
+ "context"
+ "fmt"
+ "log"
+ "net"
+ "strconv"
+ "strings"
+ "time"
+
+ xmlrpc_value "go.chromium.org/chromiumos/config/go/api/test/xmlrpc"
+ "go.chromium.org/luci/common/errors"
+
+ "infra/libs/sshpool"
+)
+
+const (
+ // Waiting 60 seconds when starting servod daemon.
+ startServodTimeout = 60
+ // Waiting 3 seconds when stopping servod daemon.
+ stopServodTimeout = 3
+ // GPIO control for USB device on servo-host
+ ImageUsbkeyDev = "image_usbkey_dev"
+ // GPIO control for USB multiplexer
+ ImageUsbkeyDirection = "image_usbkey_direction"
+ // GPIO control value that causes USB drive to be attached to DUT.
+ ImageUsbkeyTowardsDUT = "dut_sees_usbkey"
+)
+
+// status of servod daemon on servo-host.
+type status string
+
+const (
+ servodUndefined status = "UNDEFINED"
+ servodRunning status = "RUNNING"
+ servodStopping status = "STOPPING"
+ servodNotRunning status = "NOT_RUNNING"
+)
+
+// servod holds information to manage servod daemon.
+type servod struct {
+ // Servo-host hostname or IP address where servod daemon will be running.
+ host string
+ // Port allocated for running servod.
+ port int32
+ // Function to receive parameters to start servod.
+ getParams func() ([]string, error)
+ // Proxy offers forward tunnel connections via SSH to the servod instance on Servo-Host.
+ // Labs are restricted to SSH connection and port 22.
+ proxy *proxy
+}
+
+// Prepare prepares servod before call it to run commands.
+// If servod is running: do nothing.
+// If servod is not running: start servod.
+func (s *servod) Prepare(ctx context.Context, pool *sshpool.Pool) error {
+ stat, err := s.getStatus(ctx, pool)
+ if err != nil {
+ return errors.Annotate(err, "prepare servod").Err()
+ }
+ switch stat {
+ case servodNotRunning:
+ err = s.start(ctx, pool)
+ if err != nil {
+ return errors.Annotate(err, "prepare servod").Err()
+ }
+ return nil
+ case servodRunning:
+ return nil
+ }
+ return errors.Reason("prepare servod %s:%d: fail to start", s.host, s.port).Err()
+}
+
+// getStatus return status of servod daemon on the servo-host.
+func (s *servod) getStatus(ctx context.Context, pool *sshpool.Pool) (status, error) {
+ r := ssh.Run(ctx, pool, s.host, fmt.Sprintf("status servod PORT=%d", s.port))
+ if r.ExitCode == 0 {
+ if strings.Contains(strings.ToLower(r.Stdout), "start/running") {
+ return servodRunning, nil
+ } else if strings.Contains(strings.ToLower(r.Stdout), "stop/waiting") {
+ return servodStopping, nil
+ }
+ } else if strings.Contains(strings.ToLower(r.Stderr), "unknown instance") {
+ return servodNotRunning, nil
+ }
+ log.Println(ctx, "Status check: %s", r.Stderr)
+ return servodUndefined, errors.Reason("servo status %q: fail to check status", s.host).Err()
+}
+
+// start starts servod daemon on servo-host.
+func (s *servod) start(ctx context.Context, pool *sshpool.Pool) error {
+ params, err := s.getParams()
+ if err != nil {
+ return errors.Annotate(err, "start servod").Err()
+ }
+ cmd := strings.Join(append([]string{"start", "servod"}, params...), " ")
+ r := ssh.Run(ctx, pool, s.host, cmd)
+ if r.ExitCode != 0 {
+ return errors.Reason("start servod: %s", r.Stderr).Err()
+ }
+ // Waiting to start servod.
+ // TODO(otabek@): Replace to use servod tool to wait servod start.
+ log.Println(ctx, "Start servod: waiting %d seconds to initialize daemon.", startServodTimeout)
+ time.Sleep(startServodTimeout * time.Second)
+ return nil
+}
+
+// Stop stops servod daemon on servo-host.
+func (s *servod) Stop(ctx context.Context, pool *sshpool.Pool) error {
+ r := ssh.Run(ctx, pool, s.host, fmt.Sprintf("stop servod PORT=%d", s.port))
+ if r.ExitCode != 0 {
+ log.Println(ctx, "stop servod: %s", r.Stderr)
+ return errors.Reason("stop servod: %s", r.Stderr).Err()
+ } else {
+ // Wait to teardown the servod.
+ log.Println(ctx, "Stop servod: waiting %d seconds to fully teardown the daemon.", stopServodTimeout)
+ time.Sleep(stopServodTimeout * time.Second)
+ }
+ return nil
+}
+
+// Call performs execution commands by servod daemon by XMLRPC connection.
+func (s *servod) Call(ctx context.Context, pool *sshpool.Pool, method string, args []*xmlrpc_value.Value) (r *xmlrpc_value.Value, rErr error) {
+ if s.proxy == nil {
+ p, err := newProxy(pool, s.host, s.port)
+ if err != nil {
+ return nil, errors.Annotate(err, "call servod").Err()
+ }
+ s.proxy = p
+ }
+ newAddr := s.proxy.LocalAddr()
+ host, portString, err := net.SplitHostPort(newAddr)
+ if err != nil {
+ return nil, errors.Annotate(err, "call servod %q", newAddr).Err()
+ }
+ port, err := strconv.Atoi(portString)
+ if err != nil {
+ return nil, errors.Annotate(err, "call servod %q", newAddr).Err()
+ }
+ c := xmlrpc.New(host, port)
+ var iArgs []interface{}
+ for _, ra := range args {
+ iArgs = append(iArgs, ra)
+ }
+ call := xmlrpc.NewCall(method, iArgs...)
+ val := &xmlrpc_value.Value{}
+ err = c.Run(ctx, call, val)
+ if err != nil {
+ return nil, errors.Annotate(err, "call servod %q: %s", newAddr, method).Err()
+ }
+ return val, nil
+}
+
+// Close closes using resource.
+func (s *servod) Close() error {
+ if s.proxy != nil {
+ if err := s.proxy.Close(); err != nil {
+ return errors.Annotate(err, "close servod").Err()
+ }
+ }
+ return nil
+}
diff --git a/src/chromiumos/test/servod/cmd/servodserver/servodserver.go b/src/chromiumos/test/servod/cmd/servodserver/servodserver.go
index dc5ab14..66c2b1d 100644
--- a/src/chromiumos/test/servod/cmd/servodserver/servodserver.go
+++ b/src/chromiumos/test/servod/cmd/servodserver/servodserver.go
@@ -9,16 +9,21 @@
"bytes"
"context"
"errors"
+ "infra/libs/sshpool"
"io"
"log"
+ "strings"
"chromiumos/lro"
"chromiumos/test/servod/cmd/commandexecutor"
"chromiumos/test/servod/cmd/model"
+ "chromiumos/test/servod/cmd/servod"
+
+ "chromiumos/test/servod/cmd/ssh"
"go.chromium.org/chromiumos/config/go/longrunning"
"go.chromium.org/chromiumos/config/go/test/api"
- "golang.org/x/crypto/ssh"
+ crypto_ssh "golang.org/x/crypto/ssh"
)
// ServodService implementation of servod_service.proto
@@ -26,6 +31,8 @@
manager *lro.Manager
logger *log.Logger
commandexecutor commandexecutor.CommandExecutorInterface
+ sshPool *sshpool.Pool
+ servodPool *servod.Pool
}
// NewServodService creates a new servod service.
@@ -34,6 +41,8 @@
manager: lro.New(),
logger: logger,
commandexecutor: commandexecutor,
+ sshPool: sshpool.New(ssh.SSHConfig()),
+ servodPool: servod.NewPool(),
}
destructor := func() {
@@ -64,10 +73,7 @@
AllowDualV4: req.AllowDualV4,
}
- var bErr bytes.Buffer
- var err error
-
- _, bErr, err = s.RunCli(model.CliStartServod, a, nil, false)
+ _, 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{
@@ -99,10 +105,7 @@
ServodPort: req.ServodPort,
}
- var bErr bytes.Buffer
- var err error
-
- _, bErr, err = s.RunCli(model.CliStopServod, a, nil, false)
+ _, 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{
@@ -121,11 +124,14 @@
return op, err
}
-// ExecCmd executes a servod command inside the servod Docker container
-// if servod_docker_container_name parameter is provided. Otherwise, it
-// executes the command directly inside the servo host.
-// Example commands:
-// "dut-control -p $PORT power_state:off"
+// 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.Println("Received api.ExecCmdRequest: ", *req)
@@ -151,45 +157,50 @@
}, err
}
-// CallServod runs a servod command through an XML-RPC call inside the
-// servod Docker container if servod_docker_container_name parameter is provided.
-// Otherwise, it runs the command directly inside the servo host.
-// Allowed methods: doc, get, and set.
+// 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.Println("Received api.CallServodRequest: ", *req)
- a := model.CliArgs{
- ServoHostPath: req.ServoHostPath,
- ServodDockerContainerName: req.ServodDockerContainerName,
- ServodPort: req.ServodPort,
- Method: req.Method.String(),
- Args: req.Args,
- }
-
- var bOut bytes.Buffer
- var bErr bytes.Buffer
- var err error
-
- bOut, bErr, err = s.RunCli(model.CliCallServod, a, nil, false)
-
+ 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 {
- s.logger.Println("Failed to run CLI: ", err)
return &api.CallServodResponse{
Result: &api.CallServodResponse_Failure_{
Failure: &api.CallServodResponse_Failure{
- ErrorMessage: getErrorMessage(bErr, err),
- },
- },
- }, err
- } else {
- return &api.CallServodResponse{
- Result: &api.CallServodResponse_Success_{
- Success: &api.CallServodResponse_Success{
- Result: bOut.String(),
+ 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
@@ -209,7 +220,7 @@
}
// If ExitError, command ran but did not succeed
- var ee *ssh.ExitError
+ var ee *crypto_ssh.ExitError
if errors.As(runError, &ee) {
return createCommandFailedExitInfo(ee)
}
@@ -236,7 +247,7 @@
}
}
-func createCommandFailedExitInfo(err *ssh.ExitError) *api.ExecCmdResponse_ExitInfo {
+func createCommandFailedExitInfo(err *crypto_ssh.ExitError) *api.ExecCmdResponse_ExitInfo {
return &api.ExecCmdResponse_ExitInfo{
Status: int32(err.ExitStatus()),
Signaled: true,
diff --git a/src/chromiumos/test/servod/cmd/servodserver/servodserver_test.go b/src/chromiumos/test/servod/cmd/servodserver/servodserver_test.go
index 97854ec..031efdf 100644
--- a/src/chromiumos/test/servod/cmd/servodserver/servodserver_test.go
+++ b/src/chromiumos/test/servod/cmd/servodserver/servodserver_test.go
@@ -12,6 +12,7 @@
"go.chromium.org/chromiumos/config/go/longrunning"
"go.chromium.org/chromiumos/config/go/test/api"
"go.chromium.org/luci/common/errors"
+ "golang.org/x/crypto/ssh"
)
// Tests that servod starts successfully.
@@ -343,66 +344,26 @@
}
if resp.ExitInfo.Status == 0 {
- t.Fatalf("Expecting ExitInfo.Status to be not 0, instead got: %v", resp.ExitInfo.Status)
+ t.Fatalf("Expecting ExitInfo.Status to be 0, instead got: %v", resp.ExitInfo.Status)
}
}
-// Tests that calling servod is successful.
-func TestServodServer_CallServodSuccess(t *testing.T) {
+// Tests that a command execution with SSH exit failure is handled gracefully.
+func TestServodServer_ExecCmdWithSshExitFailure(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mce := mock_commandexecutor.NewMockCommandExecutorInterface(ctrl)
- mce.EXPECT().Run(gomock.Eq("servoHostPath"), gomock.Any(), gomock.Eq(nil), gomock.Eq(false)).DoAndReturn(
- func(addr string, command string, stdin io.Reader, routeToStd bool) (bytes.Buffer, bytes.Buffer, error) {
- var bOut, bErr bytes.Buffer
- bOut.Write([]byte("success!"))
- bErr.Write([]byte("not failed!"))
- return bOut, bErr, nil
- },
- )
-
- ctx := context.Background()
- var logBuf bytes.Buffer
- srv, destructor, err := NewServodService(ctx, log.New(&logBuf, "", log.LstdFlags|log.LUTC), mce)
- defer destructor()
- if err != nil {
- t.Fatalf("Failed to create new ServodService: %v", err)
- }
-
- resp, err := srv.CallServod(ctx, &api.CallServodRequest{
- ServoHostPath: "servoHostPath",
- ServodPort: 9901,
- Method: api.CallServodRequest_DOC,
- Args: "arg1",
- })
- if err != nil {
- t.Fatalf("Failed at api.CallServod: %v", err)
- }
-
- if resp.GetFailure() != nil {
- t.Fatalf("Expecting GetFailure() to be nil, instead got %v", resp.GetFailure().ErrorMessage)
- }
-
- if resp.GetSuccess().Result != "success!" {
- t.Fatalf("Expecting GetSuccess().Result to be \"success!\", instead got %v", resp.GetSuccess().Result)
- }
-}
-
-// Tests that calling servod failure is handled gracefully.
-func TestServodServer_CallServodFailure(t *testing.T) {
- ctrl := gomock.NewController(t)
- defer ctrl.Finish()
-
- mce := mock_commandexecutor.NewMockCommandExecutorInterface(ctrl)
-
- mce.EXPECT().Run(gomock.Eq("servoHostPath"), gomock.Any(), gomock.Eq(nil), gomock.Eq(false)).DoAndReturn(
+ mce.EXPECT().Run(gomock.Eq("servoHostPath"), gomock.Eq("command arg1 arg2"), gomock.Eq(nil), gomock.Eq(false)).DoAndReturn(
func(addr string, command string, stdin io.Reader, routeToStd bool) (bytes.Buffer, bytes.Buffer, error) {
var bOut, bErr bytes.Buffer
bOut.Write([]byte("not success!"))
bErr.Write([]byte("failed!"))
- return bOut, bErr, errors.Reason("error message").Err()
+ wm := ssh.Waitmsg{}
+ return bOut, bErr, &ssh.ExitError{
+ Waitmsg: wm,
+ }
},
)
@@ -414,27 +375,41 @@
t.Fatalf("Failed to create new ServodService: %v", err)
}
- resp, err := srv.CallServod(ctx, &api.CallServodRequest{
+ resp, err := srv.ExecCmd(ctx, &api.ExecCmdRequest{
ServoHostPath: "servoHostPath",
- ServodPort: 9901,
- Method: api.CallServodRequest_DOC,
- Args: "arg1",
+ Command: "command arg1 arg2",
})
if err == nil {
- t.Fatalf("Should have failed at api.CallServod.")
+ t.Fatalf("Should have failed at api.ExecCmd.")
}
- if resp.GetFailure().ErrorMessage != "failed!" {
- t.Fatalf("Expecting GetFailure().ErrorMessage to be \"failed!\", instead got %v", resp.GetFailure().ErrorMessage)
+ if string(resp.Stderr) != "failed!" {
+ t.Fatalf("Expecting Stderr to be \"failed!\", instead got %v", string(resp.Stderr))
}
- if resp.GetSuccess() != nil {
- t.Fatalf("Expecting GetSuccess() to be nil, instead got %v", resp.GetSuccess().Result)
+ if string(resp.Stdout) != "not success!" {
+ t.Fatalf("Expecting Stdout to be \"not success!\", instead got %v", string(resp.Stdout))
+ }
+
+ if resp.ExitInfo.ErrorMessage != "" {
+ t.Fatalf("Expecting ExitInfo.ErrorMessage to be \"\", instead got %v", resp.ExitInfo.ErrorMessage)
+ }
+
+ if !resp.ExitInfo.Signaled {
+ t.Fatalf("ExitInfo.Signaled should be set!")
+ }
+
+ if !resp.ExitInfo.Started {
+ t.Fatalf("ExitInfo.Started should be set!")
+ }
+
+ if resp.ExitInfo.Status != 0 {
+ t.Fatalf("Expecting ExitInfo.Status to be 0, instead got: %v", resp.ExitInfo.Status)
}
}
/*
-//NOTE: The following tests are for INTEGRATION TESTING PURPOSES and will be REMOVED before merging to master.
+//NOTE: The following tests are for INTEGRATION TESTING PURPOSES and should be REMOVED before merging to master.
var (
tls = flag.Bool("tls", false, "Connection uses TLS if true, else plain TCP")
@@ -506,7 +481,7 @@
defer conn.Close()
client := api.NewServodServiceClient(conn)
- stream, err := client.ExecCmd(context.Background(), &api.ExecCmdRequest{
+ resp, err := client.ExecCmd(context.Background(), &api.ExecCmdRequest{
ServoHostPath: "localhost:9876",
ServodDockerContainerName: "",
Command: "ps -ef | grep servod",
@@ -517,12 +492,6 @@
t.Fatalf("Failed at api.ExecCmd: %v", err)
}
- resp := &api.ExecCmdResponse{}
- err = stream.RecvMsg(resp)
- if err != nil {
- t.Fatalf("Failed at api.ExecCmd: %v", err)
- }
-
fmt.Println(resp)
}
@@ -537,7 +506,7 @@
defer conn.Close()
client := api.NewServodServiceClient(conn)
- stream, err := client.ExecCmd(context.Background(), &api.ExecCmdRequest{
+ resp, err := client.ExecCmd(context.Background(), &api.ExecCmdRequest{
ServoHostPath: "",
ServodDockerContainerName: "",
Command: "ls -ll",
@@ -548,16 +517,10 @@
t.Fatalf("Failed at api.ExecCmd: %v", err)
}
- resp := &api.ExecCmdResponse{}
- err = stream.RecvMsg(resp)
- if err != nil {
- t.Fatalf("Failed at api.ExecCmd: %v", err)
- }
-
fmt.Println(resp)
}
-func TestCallServodSuccess(t *testing.T) {
+func TestCallServodDocSuccess(t *testing.T) {
flag.Parse()
opts := getDialOptions()
@@ -568,24 +531,115 @@
defer conn.Close()
client := api.NewServodServiceClient(conn)
- stream, err := client.CallServod(context.Background(), &api.CallServodRequest{
+ resp, err := client.CallServod(context.Background(), &api.CallServodRequest{
ServoHostPath: "localhost:9876",
ServodDockerContainerName: "",
ServodPort: 9901,
Method: api.CallServodRequest_DOC,
- // Method: api.CallServodRequest_GET,
- // Method: api.CallServodRequest_SET,
- Args: "lid_openXYZ",
- // Args: "lid_open:yes",
+ Args: []*xmlrpc.Value{&xmlrpc.Value{
+ ScalarOneof: &xmlrpc.Value_String_{
+ String_: "lid_open",
+ },
+ }},
})
if err != nil {
+ fmt.Println(err)
t.Fatalf("Failed at api.CallServod: %v", err)
}
- resp := &api.CallServodResponse{}
- err = stream.RecvMsg(resp)
+ fmt.Println(resp)
+}
+
+func TestCallServodGetSuccess(t *testing.T) {
+ flag.Parse()
+ opts := getDialOptions()
+
+ conn, err := grpc.Dial(*serverAddr, opts...)
if err != nil {
+ log.Fatalf("fail to dial: %v", err)
+ }
+ defer conn.Close()
+ client := api.NewServodServiceClient(conn)
+
+ resp, err := client.CallServod(context.Background(), &api.CallServodRequest{
+ ServoHostPath: "localhost:9876",
+ ServodDockerContainerName: "",
+ ServodPort: 9901,
+ Method: api.CallServodRequest_GET,
+ Args: []*xmlrpc.Value{&xmlrpc.Value{
+ ScalarOneof: &xmlrpc.Value_String_{
+ String_: "lid_open",
+ },
+ }},
+ })
+
+ if err != nil {
+ fmt.Println(err)
+ t.Fatalf("Failed at api.CallServod: %v", err)
+ }
+
+ fmt.Println(resp)
+}
+
+func TestCallServodSetSuccess(t *testing.T) {
+ flag.Parse()
+ opts := getDialOptions()
+
+ conn, err := grpc.Dial(*serverAddr, opts...)
+ if err != nil {
+ log.Fatalf("fail to dial: %v", err)
+ }
+ defer conn.Close()
+ client := api.NewServodServiceClient(conn)
+
+ resp, err := client.CallServod(context.Background(), &api.CallServodRequest{
+ ServoHostPath: "localhost:9876",
+ ServodDockerContainerName: "",
+ ServodPort: 9901,
+ Method: api.CallServodRequest_SET,
+ Args: []*xmlrpc.Value{
+ &xmlrpc.Value{
+ ScalarOneof: &xmlrpc.Value_String_{
+ String_: "lid_open",
+ },
+ },
+ &xmlrpc.Value{
+ ScalarOneof: &xmlrpc.Value_String_{
+ String_: "yes",
+ },
+ },
+ },
+ })
+
+ if err != nil {
+ fmt.Println(err)
+ t.Fatalf("Failed at api.CallServod: %v", err)
+ }
+
+ fmt.Println(resp)
+}
+
+func TestCallServodHwinitSuccess(t *testing.T) {
+ flag.Parse()
+ opts := getDialOptions()
+
+ conn, err := grpc.Dial(*serverAddr, opts...)
+ if err != nil {
+ log.Fatalf("fail to dial: %v", err)
+ }
+ defer conn.Close()
+ client := api.NewServodServiceClient(conn)
+
+ resp, err := client.CallServod(context.Background(), &api.CallServodRequest{
+ ServoHostPath: "localhost:9876",
+ ServodDockerContainerName: "",
+ ServodPort: 9901,
+ Method: api.CallServodRequest_HWINIT,
+ })
+
+ if err != nil {
+ fmt.Println(err)
t.Fatalf("Failed at api.CallServod: %v", err)
}
diff --git a/src/chromiumos/test/servod/cmd/ssh/keys.go b/src/chromiumos/test/servod/cmd/ssh/keys.go
new file mode 100644
index 0000000..cf583e6
--- /dev/null
+++ b/src/chromiumos/test/servod/cmd/ssh/keys.go
@@ -0,0 +1,56 @@
+// Copyright 2021 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package ssh
+
+import (
+ "fmt"
+
+ "golang.org/x/crypto/ssh"
+)
+
+// The provided key is copied from
+// https://chromium.googlesource.com/chromiumos/chromite/+/master/ssh_keys/testing_rsa
+// It's a well known "private" key used widely in Chrome OS testing.
+const sshKeyContent = `
+-----BEGIN RSA PRIVATE KEY-----
+MIIEoAIBAAKCAQEAvsNpFdK5lb0GfKx+FgsrsM/2+aZVFYXHMPdvGtTz63ciRhq0
+Jnw7nln1SOcHraSz3/imECBg8NHIKV6rA+B9zbf7pZXEv20x5Ul0vrcPqYWC44PT
+tgsgvi8s0KZUZN93YlcjZ+Q7BjQ/tuwGSaLWLqJ7hnHALMJ3dbEM9fKBHQBCrG5H
+OaWD2gtXj7jp04M/WUnDDdemq/KMg6E9jcrJOiQ39IuTpas4hLQzVkKAKSrpl6MY
+2etHyoNarlWhcOwitArEDwf3WgnctwKstI/MTKB5BTpO2WXUNUv4kXzA+g8/l1al
+jIG13vtd9A/IV3KFVx/sLkkjuZ7z2rQXyNKuJwIBIwKCAQA79EWZJPh/hI0CnJyn
+16AEXp4T8nKDG2p9GpCiCGnq6u2Dvz/u1pZk97N9T+x4Zva0GvJc1vnlST7objW/
+Y8/ET8QeGSCT7x5PYDqiVspoemr3DCyYTKPkADKn+cLAngDzBXGHDTcfNP4U6xfr
+Qc5JK8BsFR8kApqSs/zCU4eqBtp2FVvPbgUOv3uUrFnjEuGs9rb1QZ0K6o08L4Cq
+N+e2nTysjp78blakZfqlurqTY6iJb0ImU2W3T8sV6w5GP1NT7eicXLO3WdIRB15a
+evogPeqtMo8GcO62wU/D4UCvq4GNEjvYOvFmPzXHvhTxsiWv5KEACtleBIEYmWHA
+POwrAoGBAOKgNRgxHL7r4bOmpLQcYK7xgA49OpikmrebXCQnZ/kZ3QsLVv1QdNMH
+Rx/ex7721g8R0oWslM14otZSMITCDCMWTYVBNM1bqYnUeEu5HagFwxjQ2tLuSs8E
+SBzEr96JLfhwuBhDH10sQqn+OQG1yj5acs4Pt3L4wlYwMx0vs1BxAoGBANd9Owro
+5ONiJXfKNaNY/cJYuLR+bzGeyp8oxToxgmM4UuA4hhDU7peg4sdoKJ4XjB9cKMCz
+ZGU5KHKKxNf95/Z7aywiIJEUE/xPRGNP6tngRunevp2QyvZf4pgvACvk1tl9B3HH
+7J5tY/GRkT4sQuZYpx3YnbdP5Y6Kx33BF7QXAoGAVCzghVQR/cVT1QNhvz29gs66
+iPIrtQnwUtNOHA6i9h+MnbPBOYRIpidGTaqEtKTTKisw79JjJ78X6TR4a9ML0oSg
+c1K71z9NmZgPbJU25qMN80ZCph3+h2f9hwc6AjLz0U5wQ4alP909VRVIX7iM8paf
+q59wBiHhyD3J16QAxhsCgYBu0rCmhmcV2rQu+kd4lCq7uJmBZZhFZ5tny9MlPgiK
+zIJkr1rkFbyIfqCDzyrU9irOTKc+iCUA25Ek9ujkHC4m/aTU3lnkNjYp/OFXpXF3
+XWZMY+0Ak5uUpldG85mwLIvATu3ivpbyZCTFYM5afSm4StmaUiU5tA+oZKEcGily
+jwKBgBdFLg+kTm877lcybQ04G1kIRMf5vAXcConzBt8ry9J+2iX1ddlu2K2vMroD
+1cP/U/EmvoCXSOGuetaI4UNQwE/rGCtkpvNj5y4twVLh5QufSOl49V0Ut0mwjPXw
+HfN/2MoO07vQrjgsFylvrw9A79xItABaqKndlmqlwMZWc9Ne
+-----END RSA PRIVATE KEY-----
+`
+
+// SSHSigner public key
+var SSHSigner ssh.Signer
+
+// InitKeys initialize key for ssh access
+func init() {
+ var err error
+ SSHSigner, err = ssh.ParsePrivateKey([]byte(sshKeyContent))
+ if err != nil {
+ panic(fmt.Sprintf("Failed to prepare key for SSH access! %s", err.Error()))
+ }
+}
diff --git a/src/chromiumos/test/servod/cmd/ssh/ssh.go b/src/chromiumos/test/servod/cmd/ssh/ssh.go
new file mode 100644
index 0000000..74b5134
--- /dev/null
+++ b/src/chromiumos/test/servod/cmd/ssh/ssh.go
@@ -0,0 +1,113 @@
+// Copyright 2021 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package ssh
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "log"
+ "time"
+
+ "golang.org/x/crypto/ssh"
+
+ "infra/libs/sshpool"
+)
+
+const (
+ defaultSSHUser = "root"
+
+ // Some tasks such as running badblocks for USB-Drive audit can
+ // take quite long (2-3 hours). We need to set timeout limit to
+ // accommodate such tasks.
+ defaultSSHTimeout = time.Hour * 3
+ DefaultPort = 22
+)
+
+// getSSHConfig provides default config for SSH.
+func SSHConfig() *ssh.ClientConfig {
+ return &ssh.ClientConfig{
+ User: defaultSSHUser,
+ HostKeyCallback: ssh.InsecureIgnoreHostKey(),
+ Timeout: defaultSSHTimeout,
+ Auth: []ssh.AuthMethod{ssh.PublicKeys(SSHSigner)},
+ }
+}
+
+// RunResult represents result of executed command.
+type RunResult struct {
+ // Command executed on the resource.
+ Command string
+ // Exit code return.
+ // Eg: 0 - everything is good
+ // 1 - executed stop with error code `1`
+ // 15 - timeout of execution
+ ExitCode int
+ // Standard output
+ Stdout string
+ // Standard error output
+ Stderr string
+}
+
+// Run executes command on the target address by SSH.
+func Run(ctx context.Context, pool *sshpool.Pool, addr string, cmd string) (result *RunResult) {
+ result = &RunResult{
+ Command: cmd,
+ ExitCode: -1,
+ }
+ if pool == nil {
+ result.Stderr = "run SSH: pool is not initialized"
+ return
+ } else if addr == "" {
+ result.Stderr = "run SSH: addr is empty"
+ return
+ } else if cmd == "" {
+ result.Stderr = fmt.Sprintf("run SSH %q: cmd is empty", addr)
+ return
+ }
+ sc, err := pool.GetContext(ctx, addr)
+ if err != nil {
+ result.Stderr = fmt.Sprintf("run SSH %q: fail to get client from pool; %s", addr, err)
+ return
+ }
+ defer func() { pool.Put(addr, sc) }()
+ result = internalRunSSH(cmd, sc)
+ log.Println(ctx, "run SSH %q: Cmd: %q; ExitCode: %d; Stdout: %q; Stderr: %q", addr, result.Command, result.ExitCode, result.Stdout, result.Stderr)
+ return
+}
+
+func internalRunSSH(cmd string, client *ssh.Client) (result *RunResult) {
+ result = &RunResult{
+ Command: cmd,
+ ExitCode: -1,
+ }
+ session, err := client.NewSession()
+ if err != nil {
+ result.Stderr = fmt.Sprintf("internal run SSH: %s", err)
+ return
+ }
+ defer func() { session.Close() }()
+ var stdOut, stdErr bytes.Buffer
+ session.Stdout = &stdOut
+ session.Stderr = &stdErr
+
+ err = session.Run(cmd)
+
+ result.Stdout = stdOut.String()
+ result.Stderr = stdErr.String()
+ if err == nil {
+ result.ExitCode = 0
+ } else if exitErr, ok := err.(*ssh.ExitError); ok {
+ result.ExitCode = exitErr.ExitStatus()
+ } else if _, ok := err.(*ssh.ExitMissingError); ok {
+ result.ExitCode = -2
+ result.Stderr = err.Error()
+ } else {
+ // Set error 1 as not expected exit.
+ result.ExitCode = -3
+ result.Stderr = err.Error()
+ }
+ return
+}
diff --git a/src/chromiumos/test/servod/cmd/xmlrpc/xmlrpc.go b/src/chromiumos/test/servod/cmd/xmlrpc/xmlrpc.go
new file mode 100644
index 0000000..6cbb404
--- /dev/null
+++ b/src/chromiumos/test/servod/cmd/xmlrpc/xmlrpc.go
@@ -0,0 +1,531 @@
+// Copyright 2021 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Package xmlrpc implements the XML-RPC client library.
+package xmlrpc
+
+import (
+ "bytes"
+ "context"
+ "encoding/xml"
+ "fmt"
+ "io/ioutil"
+ "math"
+ "net/http"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+
+ "go.chromium.org/chromiumos/config/go/api/test/xmlrpc"
+ "go.chromium.org/luci/common/errors"
+)
+
+const (
+ // defaultRPCTimeout is expected time to get response from servo for most regular commands.
+ defaultRPCTimeout = 10 * time.Second
+ // maxRPCTimeout is very long, because some of the servo calls are really slow.
+ maxRPCTimeout = 1 * time.Hour
+)
+
+// XMLRpc holds the XML-RPC information.
+type XMLRpc struct {
+ host string
+ port int
+}
+
+// New creates a new XMLRpc object for communicating with XML-RPC server.
+func New(host string, port int) *XMLRpc {
+ return &XMLRpc{host: host, port: port}
+}
+
+// Call represents a XML-RPC call request.
+type Call struct {
+ method string
+ args []interface{}
+ timeout time.Duration
+}
+
+// methodCall mirrors the structure of an XML-RPC method call.
+type methodCall struct {
+ XMLName xml.Name `xml:"methodCall"`
+ MethodName string `xml:"methodName"`
+ Params *[]param `xml:"params>param"`
+}
+
+// methodResponse is an XML-RPC response.
+type methodResponse struct {
+ XMLName xml.Name `xml:"methodResponse"`
+ Params *[]param `xml:"params>param,omitempty"`
+ Fault *fault `xml:"fault,omitempty"`
+}
+
+// param is an XML-RPC param.
+type param struct {
+ Value value `xml:"value"`
+}
+
+// fault is an XML-RPC fault.
+// If present, it usually contains in its value a struct of two members:
+// faultCode (an int) and faultString (a string).
+type fault struct {
+ Value value `xml:"value"`
+}
+
+// Value is an XML-RPC value.
+type value struct {
+ Boolean *string `xml:"boolean,omitempty"`
+ Double *string `xml:"double,omitempty"`
+ Int *string `xml:"int,omitempty"`
+ Str *string `xml:"string,omitempty"`
+ Array *xmlArray `xml:"array,omitempty"`
+ Struct *xmlStruct `xml:"struct,omitempty"`
+}
+
+// xmlArray is an XML-RPC array.
+type xmlArray struct {
+ Values []value `xml:"data>value,omitempty"`
+}
+
+// xmlStruct is an XML-RPC struct.
+type xmlStruct struct {
+ Members []member `xml:"member,omitempty"`
+}
+
+// member is an XML-RPC object containing a name and a value.
+type member struct {
+ Name string `xml:"name"`
+ Value value `xml:"value"`
+}
+
+// String implements the String() interface of value.
+// Example:
+// "test-string" : (string)test-string
+// 4 : (int)4
+// 1.25 : (double)1.25
+func (v value) String() string {
+ if v.Boolean != nil {
+ return "(boolean)" + *v.Boolean
+ }
+ if v.Double != nil {
+ return "(double)" + *v.Double
+ }
+ if v.Int != nil {
+ return "(int)" + *v.Int
+ }
+ if v.Str != nil {
+ return "(string)" + *v.Str
+ }
+ if v.Array != nil {
+ var values []string
+ for _, e := range v.Array.Values {
+ values = append(values, e.String())
+ }
+ return "[" + strings.Join(values, ", ") + "]"
+ }
+ if v.Struct != nil {
+ var values []string
+ for _, m := range v.Struct.Members {
+ values = append(values, fmt.Sprintf("%s: %s", m.Name, m.Value.String()))
+ }
+ return "{" + strings.Join(values, ", ") + "}"
+ }
+ return "<empty>"
+}
+
+// FaultError is a type of error representing an XML-RPC fault.
+type FaultError struct {
+ Code int
+ Reason string
+}
+
+// Error represent error message generation.
+func (e FaultError) Error() string {
+ return fmt.Sprintf("Fault %d:%s", e.Code, e.Reason)
+}
+
+// NewFaultError creates a FaultError.
+func NewFaultError(code int, reason string) FaultError {
+ return FaultError{
+ Code: code,
+ Reason: reason,
+ }
+}
+
+// xmlBooleanToBool converts the strings '1' or '0' into boolean.
+func xmlBooleanToBool(xmlBool string) (bool, error) {
+ if len(xmlBool) != 1 {
+ return false, errors.Reason("xmlBooleanToBool got %q; expected '1' or '0'", xmlBool).Err()
+ }
+ switch xmlBool[0] {
+ case '1':
+ return true, nil
+ case '0':
+ return false, nil
+ default:
+ return false, errors.Reason("xmlBooleanToBool got %q; expected '1' or '0'", xmlBool).Err()
+ }
+}
+
+// boolToXMLBoolean converts a Go boolean to an XML-RPC boolean string.
+func boolToXMLBoolean(v bool) string {
+ if v {
+ return "1"
+ }
+ return "0"
+}
+
+// xmlIntegerToInt converts numeric strings such as '-1' into integers.
+func xmlIntegerToInt(xmlInt string) (int, error) {
+ if len(xmlInt) == 0 {
+ return 0, errors.New("xmlIntegerToInt got empty xml value")
+ }
+ i, err := strconv.ParseInt(xmlInt, 10, 32)
+ if err != nil {
+ return 0, err
+ }
+ return int(i), nil
+}
+
+// intToXMLInteger converts a Go integer to an XML-RPC integer string.
+func intToXMLInteger(i int) (string, error) {
+ if i > math.MaxInt32 || i < math.MinInt32 {
+ return "", errors.Reason("intToXMLInteger needs a value that can fit in an int32: got %d, want between %d and %d", i, math.MinInt32, math.MaxInt32).Err()
+ }
+ return strconv.FormatInt(int64(i), 10), nil
+}
+
+// xmlDoubleToFloat64 converts double-like strings such as "1.5" into float64s.
+func xmlDoubleToFloat64(s string) (float64, error) {
+ if len(s) == 0 {
+ return 0.0, errors.Reason("xmlDoubleToFloat64 got empty xml value").Err()
+ }
+ return strconv.ParseFloat(s, 64)
+}
+
+// float64ToXMLDouble converts a Go float64 to an XML-RPC double-like string.
+func float64ToXMLDouble(f float64) string {
+ return strconv.FormatFloat(f, 'f', -1, 64)
+}
+
+// newValue creates an XML-RPC <value>.
+func newValue(in interface{}) (value, error) {
+ if reflect.TypeOf(in).Kind() == reflect.Slice || reflect.TypeOf(in).Kind() == reflect.Array {
+ v := reflect.ValueOf(in)
+ var a xmlArray
+ for i := 0; i < v.Len(); i++ {
+ val, err := newValue(v.Index(i).Interface())
+ if err != nil {
+ return value{}, err
+ }
+ a.Values = append(a.Values, val)
+ }
+ return value{Array: &a}, nil
+ }
+ switch v := in.(type) {
+ case string:
+ s := v
+ return value{Str: &s}, nil
+ case bool:
+ b := boolToXMLBoolean(v)
+ return value{Boolean: &b}, nil
+ case int:
+ i, err := intToXMLInteger(v)
+ if err != nil {
+ return value{}, err
+ }
+ return value{Int: &i}, nil
+ case float64:
+ f := float64ToXMLDouble(v)
+ return value{Double: &f}, nil
+ case *xmlrpc.Value:
+ sv := v.GetScalarOneof()
+ if x, ok := sv.(*xmlrpc.Value_Int); ok {
+ return newValue(x.Int)
+ } else if x, ok := sv.(*xmlrpc.Value_Boolean); ok {
+ return newValue(x.Boolean)
+ } else if x, ok := sv.(*xmlrpc.Value_String_); ok {
+ return newValue(x.String_)
+ } else if x, ok := sv.(*xmlrpc.Value_Double); ok {
+ return newValue(x.Double)
+ }
+ }
+ return value{}, errors.Reason("%q is not a supported type for newValue", reflect.TypeOf(in)).Err()
+}
+
+// newParams creates a list of XML-RPC <params>.
+func newParams(args []interface{}) ([]param, error) {
+ var params []param
+ for _, arg := range args {
+ v, err := newValue(arg)
+ if err != nil {
+ return nil, err
+ }
+ params = append(params, param{v})
+ }
+ return params, nil
+}
+
+// NewCall creates a XML-RPC call.
+func NewCall(method string, args ...interface{}) Call {
+ return NewCallTimeout(method, defaultRPCTimeout, args...)
+}
+
+// NewCallTimeout creates a XML-RPC call.
+func NewCallTimeout(method string, timeout time.Duration, args ...interface{}) Call {
+ return Call{
+ method: method,
+ args: args,
+ timeout: timeout,
+ }
+}
+
+// serializeMethodCall turns a method and args into a serialized XML-RPC method call.
+func serializeMethodCall(cl Call) ([]byte, error) {
+ params, err := newParams(cl.args)
+ if err != nil {
+ return nil, err
+ }
+ return xml.Marshal(&methodCall{MethodName: cl.method, Params: ¶ms})
+}
+
+// getTimeout returns the lowest of the default timeout or remaining duration
+// to the context's deadline.
+func getTimeout(ctx context.Context, cl Call) time.Duration {
+ timeout := cl.timeout
+ if timeout > maxRPCTimeout {
+ timeout = maxRPCTimeout
+ }
+ if dl, ok := ctx.Deadline(); ok {
+ newTimeout := dl.Sub(time.Now())
+ if newTimeout < timeout {
+ timeout = newTimeout
+ }
+ }
+ return timeout
+}
+
+// unpackValue unpacks a value struct into the given pointers.
+func unpackValue(val value, out interface{}) error {
+ switch o := out.(type) {
+ case *string:
+ if val.Str == nil {
+ return errors.Reason("value %s is not a string value", val).Err()
+ }
+ *o = *val.Str
+ case *bool:
+ if val.Boolean == nil {
+ return errors.Reason("value %s is not a boolean value", val).Err()
+ }
+ v, err := xmlBooleanToBool(*val.Boolean)
+ if err != nil {
+ return err
+ }
+ *o = v
+ case *int:
+ if val.Int == nil {
+ return errors.Reason("value %s is not an int value", val).Err()
+ }
+ i, err := xmlIntegerToInt(*val.Int)
+ if err != nil {
+ return err
+ }
+ *o = i
+ case *float64:
+ if val.Double == nil {
+ return errors.Reason("value %s is not a double value", val).Err()
+ }
+ f, err := xmlDoubleToFloat64(*val.Double)
+ if err != nil {
+ return err
+ }
+ *o = f
+ case *[]string:
+ if val.Array == nil {
+ return errors.Reason("value %s is not an array value", val).Err()
+ }
+ for _, e := range val.Array.Values {
+ var i string
+ if err := unpackValue(e, &i); err != nil {
+ return err
+ }
+ *o = append(*o, i)
+ }
+ case *[]bool:
+ if val.Array == nil {
+ return errors.Reason("value %s is not an array value", val).Err()
+ }
+ for _, e := range val.Array.Values {
+ var i bool
+ if err := unpackValue(e, &i); err != nil {
+ return err
+ }
+ *o = append(*o, i)
+ }
+ case *[]int:
+ if val.Array == nil {
+ return errors.Reason("value %s is not an array value", val).Err()
+ }
+ for _, e := range val.Array.Values {
+ var i int
+ if err := unpackValue(e, &i); err != nil {
+ return err
+ }
+ *o = append(*o, i)
+ }
+ case *[]float64:
+ if val.Array == nil {
+ return errors.Reason("value %s is not an array value", val).Err()
+ }
+ for _, e := range val.Array.Values {
+ var i float64
+ if err := unpackValue(e, &i); err != nil {
+ return err
+ }
+ *o = append(*o, i)
+ }
+ case *xmlrpc.Value:
+ if val.Str != nil {
+ o.ScalarOneof = &xmlrpc.Value_String_{
+ String_: *val.Str,
+ }
+ } else if val.Boolean != nil {
+ v, err := xmlBooleanToBool(*val.Boolean)
+ if err != nil {
+ return err
+ }
+ o.ScalarOneof = &xmlrpc.Value_Boolean{
+ Boolean: v,
+ }
+ } else if val.Int != nil {
+ i, err := xmlIntegerToInt(*val.Int)
+ if err != nil {
+ return err
+ }
+ o.ScalarOneof = &xmlrpc.Value_Int{
+ Int: int32(i),
+ }
+ } else if val.Double != nil {
+ f, err := xmlDoubleToFloat64(*val.Double)
+ if err != nil {
+ return err
+ }
+ o.ScalarOneof = &xmlrpc.Value_Double{
+ Double: f,
+ }
+ } else if val.Array != nil {
+ vl := make([]*xmlrpc.Value, len(val.Array.Values))
+ o.ScalarOneof = &xmlrpc.Value_Array{
+ Array: &xmlrpc.Array{
+ Values: vl,
+ },
+ }
+ for i, v := range val.Array.Values {
+ vl[i] = &xmlrpc.Value{}
+ if err := unpackValue(v, vl[i]); err != nil {
+ return err
+ }
+ }
+ } else {
+ return errors.Reason("not implemented type of *xmlrpc.Value, %#v", val).Err()
+ }
+ default:
+ return errors.Reason("%q is not a supported type for unpackValue %#v", reflect.TypeOf(out), out).Err()
+ }
+ return nil
+}
+
+// unpack extracts a response's arguments into a list of given pointers.
+func (r *methodResponse) unpack(out []interface{}) error {
+ if r.Params == nil {
+ if len(out) != 0 {
+ return errors.Reason("response contains no args; want %d", len(out)).Err()
+ }
+ return nil
+ }
+ if len(*r.Params) != len(out) {
+ return errors.Reason("response contains %d arg(s); want %d", len(*r.Params), len(out)).Err()
+ }
+
+ for i, p := range *r.Params {
+ if err := unpackValue(p.Value, out[i]); err != nil {
+ return errors.Annotate(err, "failed to unpack response param at index %d", i).Err()
+ }
+ }
+
+ return nil
+}
+
+// checkFault returns a FaultError if the response contains a fault with a non-zero faultCode.
+func (r *methodResponse) checkFault() error {
+ if r.Fault == nil {
+ return nil
+ }
+ if r.Fault.Value.Struct == nil {
+ return errors.Reason("fault %s doesn't contain xml-rpc struct", r.Fault.Value).Err()
+ }
+ var rawFaultCode string
+ var faultString string
+ for _, m := range r.Fault.Value.Struct.Members {
+ switch m.Name {
+ case "faultCode":
+ if m.Value.Int == nil {
+ return errors.Reason("faultCode %s doesn't provide integer value", m.Value).Err()
+ }
+ rawFaultCode = *m.Value.Int
+ case "faultString":
+ if m.Value.Str == nil {
+ return errors.Reason("faultString %s doesn't provide string value", m.Value).Err()
+ }
+ faultString = *m.Value.Str
+ default:
+ return errors.Reason("unexpected fault member name: %s", m.Name).Err()
+ }
+ }
+ faultCode, err := xmlIntegerToInt(rawFaultCode)
+ if err != nil {
+ return errors.Annotate(err, "interpreting fault code").Err()
+ }
+ if faultCode == 0 {
+ return errors.Reason("response contained a fault with unexpected code 0; want non-0: faultString=%s", faultString).Err()
+ }
+ return NewFaultError(faultCode, faultString)
+}
+
+// Run makes an XML-RPC call to the server.
+func (r *XMLRpc) Run(ctx context.Context, cl Call, out ...interface{}) error {
+ body, err := serializeMethodCall(cl)
+ if err != nil {
+ return err
+ }
+
+ // Get RPC timeout duration from context or use default.
+ timeout := getTimeout(ctx, cl)
+ serverURL := fmt.Sprintf("http://%s:%d", r.host, r.port)
+ httpClient := &http.Client{Timeout: timeout}
+ resp, err := httpClient.Post(serverURL, "text/xml", bytes.NewBuffer(body))
+ if err != nil {
+ return errors.Annotate(err, "timeout = %v", timeout).Err()
+ }
+ defer func() { resp.Body.Close() }()
+
+ // Read body and unmarshal XML.
+ bodyBytes, err := ioutil.ReadAll(resp.Body)
+ res := methodResponse{}
+ if err = xml.Unmarshal(bodyBytes, &res); err != nil {
+ return err
+ }
+ if err = res.checkFault(); err != nil {
+ return err
+ }
+ // If outs are specified, unpack response params.
+ // Otherwise, return without unpacking.
+ if len(out) > 0 {
+ if err := res.unpack(out); err != nil {
+ // testing.ContextLogf(ctx, "Failed to unpack XML-RPC response for request %v: %s", cl, string(bodyBytes))
+ return err
+ }
+ }
+ return nil
+}
diff --git a/src/chromiumos/test/servod/cmd/xmlrpc/xmlrpc_test.go b/src/chromiumos/test/servod/cmd/xmlrpc/xmlrpc_test.go
new file mode 100644
index 0000000..33f68e0
--- /dev/null
+++ b/src/chromiumos/test/servod/cmd/xmlrpc/xmlrpc_test.go
@@ -0,0 +1,619 @@
+// Copyright 2021 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package xmlrpc
+
+import (
+ "encoding/xml"
+ "fmt"
+ "math"
+ "reflect"
+ "strconv"
+ "testing"
+)
+
+func TestXMLBooleanToBool(t *testing.T) {
+ for _, tc := range []struct {
+ input string
+ expected bool
+ expectErr bool
+ }{
+ {"1", true, false},
+ {"0", false, false},
+ {"rutabaga", false, true},
+ } {
+ actual, err := xmlBooleanToBool(tc.input)
+ if tc.expectErr {
+ if err == nil {
+ t.Errorf("xmlBooleanToBool(%q) unexpectedly succeeded", tc.input)
+ }
+ } else {
+ if err != nil {
+ t.Errorf("xmlBooleanToBool(%q) failed: %v", tc.input, err)
+ }
+ if actual != tc.expected {
+ t.Errorf("xmlBooleanToBool(%q) = %v; want %v", tc.input, actual, tc.expected)
+ }
+ }
+ }
+}
+
+func TestBoolToXMLBoolean(t *testing.T) {
+ for _, tc := range []struct {
+ input bool
+ expected string
+ }{
+ {true, "1"},
+ {false, "0"},
+ } {
+ actual := boolToXMLBoolean(tc.input)
+ if actual != tc.expected {
+ t.Errorf("boolToXMLBoolean(%v) = %q; want %q", tc.input, actual, tc.expected)
+ }
+ }
+}
+
+func TestXMLIntegerToInt(t *testing.T) {
+ for _, tc := range []struct {
+ input string
+ expected int
+ expectErr bool
+ }{
+ {"1", 1, false},
+ {"0", 0, false},
+ {"-1", -1, false},
+ {"1.5", 0, true},
+ {"3000000000", 0, true}, // too big for an int32
+ {"easter-egg", 0, true},
+ {"true", 0, true},
+ } {
+ actual, err := xmlIntegerToInt(tc.input)
+ if tc.expectErr {
+ if err == nil {
+ t.Errorf("xmlIntegerToInt(%q) unexpectedly succeeded", tc.input)
+ }
+ continue
+ }
+ if err != nil {
+ t.Errorf("xmlIntegerToInt(%q) failed: %v", tc.input, err)
+ continue
+ }
+ if actual != tc.expected {
+ t.Errorf("xmlIntegerToInt(%q) = %v; want %v", tc.input, actual, tc.expected)
+ }
+ }
+}
+
+func TestIntToXMLInteger(t *testing.T) {
+ for _, tc := range []struct {
+ input int
+ expected string
+ expectErr bool
+ }{
+ {1, "1", false},
+ {0, "0", false},
+ {-1, "-1", false},
+ {math.MaxInt64, "", true},
+ } {
+ actual, err := intToXMLInteger(tc.input)
+ if tc.expectErr {
+ if err == nil {
+ t.Errorf("intToXMLInteger(%d) unexpectedly succeeded", tc.input)
+ continue
+ }
+ }
+ if actual != tc.expected {
+ t.Errorf("IntToXMLInteger(%v) = %q; want %q", tc.input, actual, tc.expected)
+ }
+ }
+}
+
+func TestXMLDoubleToFloat64(t *testing.T) {
+ for _, tc := range []struct {
+ input string
+ expected float64
+ expectErr bool
+ }{
+ {"1", 1.0, false},
+ {"3.14", 3.14, false},
+ {"-3.14", -3.14, false},
+ {strconv.FormatFloat(math.MaxFloat64, 'f', -1, 64), math.MaxFloat64, false},
+ {strconv.FormatFloat(math.SmallestNonzeroFloat64, 'f', -1, 64), math.SmallestNonzeroFloat64, false},
+ {"", 0.0, true},
+ {"easter-egg", 0.0, true},
+ {"true", 0.0, true},
+ } {
+ actual, err := xmlDoubleToFloat64(tc.input)
+ if tc.expectErr {
+ if err == nil {
+ t.Errorf("xmlDoubleToFloat64(%q) unexpectedly succeeded", tc.input)
+ }
+ continue
+ }
+ if err != nil {
+ t.Errorf("xmlDoubleToFloat64(%q) failed: %v", tc.input, err)
+ continue
+ }
+ if actual != tc.expected {
+ t.Errorf("xmlDoubleToFloat64(%q) = %v; want %v", tc.input, actual, tc.expected)
+ }
+ }
+}
+
+func TestFloat64ToXMLDouble(t *testing.T) {
+ for _, tc := range []struct {
+ input float64
+ expected string
+ }{
+ {3.14, "3.14"},
+ {0, "0"},
+ {-1, "-1"},
+ } {
+ actual := float64ToXMLDouble(tc.input)
+ if actual != tc.expected {
+ t.Errorf("float64ToXMLDouble(%v) = %q; want %q", tc.input, actual, tc.expected)
+ }
+ }
+}
+
+func TestValueString(t *testing.T) {
+ s := "1"
+ v := value{Int: &s}
+ sOut := fmt.Sprintf("%v", v)
+ sWant := "(int)1"
+ if sOut != sWant {
+ t.Errorf("String() got %q, want %q", sOut, sWant)
+ }
+
+ s = "1"
+ v = value{Boolean: &s}
+ sOut = fmt.Sprintf("%v", v)
+ sWant = "(boolean)1"
+ if sOut != sWant {
+ t.Errorf("String() got %q, want %q", sOut, sWant)
+ }
+
+ s = "1.2"
+ v = value{Double: &s}
+ sOut = fmt.Sprintf("%v", v)
+ sWant = "(double)1.2"
+ if sOut != sWant {
+ t.Errorf("String() got %q, want %q", sOut, sWant)
+ }
+
+ s = "1.2"
+ v = value{Str: &s}
+ sOut = fmt.Sprintf("%v", v)
+ sWant = "(string)1.2"
+ if sOut != sWant {
+ t.Errorf("String() got %q, want %q", sOut, sWant)
+ }
+
+ s1 := "2"
+ s2 := "0"
+ s3 := "2.1"
+ s4 := "2.1"
+ a := xmlArray{Values: []value{
+ {Int: &s1},
+ {Boolean: &s2},
+ {Double: &s3},
+ {Str: &s4},
+ }}
+ v = value{Array: &a}
+ sOut = fmt.Sprintf("%v", v)
+ sWant = "[(int)2, (boolean)0, (double)2.1, (string)2.1]"
+ if sOut != sWant {
+ t.Errorf("String() got %q, want %q", sOut, sWant)
+ }
+
+ b := xmlStruct{Members: []member{
+ {Name: "key1", Value: value{Int: &s1}},
+ {Name: "key2", Value: value{Boolean: &s2}},
+ {Name: "key3", Value: value{Double: &s3}},
+ {Name: "key4", Value: value{Str: &s4}},
+ }}
+ v = value{Struct: &b}
+ sOut = fmt.Sprintf("%v", v)
+ sWant = "{key1: (int)2, key2: (boolean)0, key3: (double)2.1, key4: (string)2.1}"
+ if sOut != sWant {
+ t.Errorf("String() got %q, want %q", sOut, sWant)
+ }
+}
+
+func TestNewValue(t *testing.T) {
+ expectedStr := "rutabaga"
+ v, err := newValue(expectedStr)
+ if err != nil {
+ t.Errorf("newValue(%q) failed: %v", expectedStr, err)
+ return
+ }
+ if *v.Str != expectedStr {
+ t.Errorf("got %q; want %q", *v.Str, expectedStr)
+ }
+
+ expectedBool := true
+ expectedBoolStr := "1"
+ v, err = newValue(expectedBool)
+ if err != nil {
+ t.Errorf("input %v gave unexpected error: %v", expectedStr, err)
+ return
+ }
+ if *v.Boolean != expectedBoolStr {
+ t.Errorf("got %q; want %q", *v.Boolean, expectedBoolStr)
+ }
+
+ expectedInt := -1
+ expectedIntStr := "-1"
+ v, err = newValue(expectedInt)
+ if err != nil {
+ t.Errorf("input %v gave unexpected error: %v", expectedInt, err)
+ return
+ }
+ if *v.Int != expectedIntStr {
+ t.Errorf("got %q; want %q", *v.Int, expectedIntStr)
+ }
+
+ f := -3.14
+ expectedDoubleStr := "-3.14"
+ v, err = newValue(f)
+ if err != nil {
+ t.Errorf("input %f gave unexpected error: %v", f, err)
+ return
+ }
+ if *v.Double != expectedDoubleStr {
+ t.Errorf("got %q; want %q", *v.Double, expectedDoubleStr)
+ }
+
+ arrInt := []int{1, 2}
+ s1 := "1"
+ s2 := "2"
+ expectedArrayOfInt := xmlArray{Values: []value{{Int: &s1}, {Int: &s2}}}
+ v, err = newValue(arrInt)
+ if err != nil {
+ t.Errorf("input %v gave unexpected error: %v", arrInt, err)
+ return
+ }
+ if !reflect.DeepEqual(*v.Array, expectedArrayOfInt) {
+ t.Errorf("got %v; want %v", v.Array, expectedArrayOfInt)
+ }
+
+ expectedUnsupported := struct{}{}
+ v, err = newValue(expectedUnsupported)
+ if err == nil {
+ t.Errorf("input %v did not throw expected error", expectedUnsupported)
+ }
+}
+
+func TestNewParams(t *testing.T) {
+ actual, err := newParams([]interface{}{"rutabaga", true, -3.14})
+
+ if err != nil {
+ t.Errorf("got unexpected error: %v", err)
+ return
+ }
+ if len(actual) != 3 {
+ t.Errorf("got len %d; want %d", len(actual), 3)
+ }
+ if *actual[0].Value.Str != "rutabaga" {
+ t.Errorf("for first return value got %q; want %q", *actual[0].Value.Str, "rutabaga")
+ }
+ if *actual[1].Value.Boolean != "1" {
+ t.Errorf("for second return value got %q; want %q", *actual[1].Value.Boolean, "1")
+ }
+ if *actual[2].Value.Double != "-3.14" {
+ t.Errorf("for third return value got %q; want %q", *actual[2].Value.Double, "-3.14")
+ }
+}
+
+func TestSerializeMethodCall(t *testing.T) {
+ cl := NewCall("TestMethod", 1, false, 2.2, "lucky",
+ []int{1, 2}, []bool{false, true}, []float64{1.1, 2.2}, []string{"so", "lucky"})
+ body, err := serializeMethodCall(cl)
+ if err != nil {
+ t.Fatal("Failed to serialize call: ", cl)
+ }
+
+ expectedBody := `<methodCall>` +
+ `<methodName>TestMethod</methodName>` +
+ `<params>` +
+ `<param><value><int>1</int></value></param>` +
+ `<param><value><boolean>0</boolean></value></param>` +
+ `<param><value><double>2.2</double></value></param>` +
+ `<param><value><string>lucky</string></value></param>` +
+ `<param><value><array><data><value><int>1</int></value><value><int>2</int></value></data></array></value></param>` +
+ `<param><value><array><data><value><boolean>0</boolean></value><value><boolean>1</boolean></value></data></array></value></param>` +
+ `<param><value><array><data><value><double>1.1</double></value><value><double>2.2</double></value></data></array></value></param>` +
+ `<param><value><array><data><value><string>so</string></value><value><string>lucky</string></value></data></array></value></param>` +
+ `</params>` +
+ `</methodCall>`
+
+ if string(body) != expectedBody {
+ t.Errorf("serializeMethodCall: got\n%s\nwant\n%s", string(body), expectedBody)
+ }
+}
+
+func TestUnpack(t *testing.T) {
+ resp := methodResponse{Params: nil}
+ var out string
+ expErr := "response contains no args; want 1"
+ if err := resp.unpack([]interface{}{&out}); err == nil {
+ t.Errorf("unpacking got no error")
+ } else if err.Error() != expErr {
+ t.Errorf("unpacking got %q; want %q", err.Error(), expErr)
+ }
+ if err := resp.unpack([]interface{}{}); err != nil {
+ t.Errorf("unpacking got error %v", err)
+ }
+
+ arrIntIn := []int{1, 2}
+ params, err := newParams([]interface{}{"rutabaga", true, 1, -3.14, arrIntIn})
+ if err != nil {
+ t.Fatal("creating params: ", err)
+ }
+ resp = methodResponse{Params: ¶ms}
+ var stringOut string
+ var boolOut bool
+ var intOut int
+ var floatOut float64
+ var arrIntOut []int
+ if err := resp.unpack([]interface{}{&stringOut, &boolOut, &intOut, &floatOut, &arrIntOut}); err != nil {
+ t.Fatal("unpacking:", err)
+ }
+ if stringOut != "rutabaga" {
+ t.Errorf("unpacking %q: got %q; want %q", "rutabaga", stringOut, "rutabaga")
+ }
+ if boolOut != true {
+ t.Errorf("unpacking %q: got %v; want %v", "true", boolOut, true)
+ }
+ if intOut != 1 {
+ t.Errorf("unpacking %q: got %d; want %d", "1", intOut, 1)
+ }
+ if floatOut != -3.14 {
+ t.Errorf("unpacking %q: got %f; want %f", "-3.14", floatOut, -3.14)
+ }
+ if !reflect.DeepEqual(arrIntIn, arrIntOut) {
+ t.Errorf("unpacking %v: got %v", arrIntIn, arrIntOut)
+ }
+}
+
+func TestXMLResponse(t *testing.T) {
+ xmlStr := `
+ <?xml version="1.0"?>
+ <methodResponse>
+ <params>
+ <param>
+ <value><double>3.14</double></value>
+ </param>
+ <param>
+ <value><int>1</int></value>
+ </param>
+ <param>
+ <value><string>hellow world</string></value>
+ </param>
+ <param>
+ <value><boolean>0</boolean></value>
+ </param>
+ <param>
+ <value>
+ <array>
+ <data>
+ <value><int>10</int></value>
+ <value><int>20</int></value>
+ </data>
+ </array>
+ </value>
+ </param>
+ <param>
+ <value>
+ <array>
+ <data>
+ <value><boolean>0</boolean></value>
+ <value><boolean>1</boolean></value>
+ </data>
+ </array>
+ </value>
+ </param>
+ <param>
+ <value>
+ <array>
+ <data>
+ <value><double>1.1</double></value>
+ <value><double>2.2</double></value>
+ </data>
+ </array>
+ </value>
+ </param>
+ <param>
+ <value>
+ <array>
+ <data>
+ <value><string>10</string></value>
+ <value><string>20</string></value>
+ </data>
+ </array>
+ </value>
+ </param>
+ </params>
+ </methodResponse>
+ `
+ res := methodResponse{}
+ if err := xml.Unmarshal([]byte(xmlStr), &res); err != nil {
+ t.Fatal("xml unmarshal:", err)
+ }
+ var stringOut string
+ var boolOut bool
+ var intOut int
+ var floatOut float64
+ var arrIntOut []int
+ var arrBoolOut []bool
+ var arrDoubleOut []float64
+ var arrStrOut []string
+ if err := res.unpack([]interface{}{&floatOut, &intOut, &stringOut, &boolOut,
+ &arrIntOut, &arrBoolOut, &arrDoubleOut, &arrStrOut}); err != nil {
+ t.Fatal("response unpack:", err)
+ }
+ if floatOut != 3.14 {
+ t.Errorf("unpacking %q: got %f; want %f", "3.14", floatOut, 3.14)
+ }
+ if intOut != 1 {
+ t.Errorf("unpacking %q: got %d; want %d", "1", intOut, 1)
+ }
+ if stringOut != "hellow world" {
+ t.Errorf("unpacking %q: got %q; want %q", "hellow world", stringOut, "hellow world")
+ }
+ if boolOut != false {
+ t.Errorf("unpacking %q: got %v; want %v", "false", boolOut, false)
+ }
+ arrIntIn := []int{10, 20}
+ if !reflect.DeepEqual(arrIntIn, arrIntOut) {
+ t.Errorf("unpacking %v: got %v", arrIntIn, arrIntOut)
+ }
+ arrBoolIn := []bool{false, true}
+ if !reflect.DeepEqual(arrBoolIn, arrBoolOut) {
+ t.Errorf("unpacking %v: got %v", arrBoolIn, arrBoolOut)
+ }
+ arrDoubleIn := []float64{1.1, 2.2}
+ if !reflect.DeepEqual(arrDoubleIn, arrDoubleOut) {
+ t.Errorf("unpacking %v: got %v", arrDoubleIn, arrDoubleOut)
+ }
+ arrStrIn := []string{"10", "20"}
+ if !reflect.DeepEqual(arrStrIn, arrStrOut) {
+ t.Errorf("unpacking %v: got %v", arrStrIn, arrStrOut)
+ }
+}
+
+func TestCheckFault(t *testing.T) {
+ hasFault := []byte(`<?xml version='1.0'?>
+ <methodResponse>
+ <fault>
+ <value>
+ <struct>
+ <member>
+ <name>faultCode</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>faultString</name>
+ <value>
+ <string>Bad XML request!</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </fault>
+ </methodResponse>`)
+ noFault := []byte(`<?xml version='1.0'?>
+ <methodResponse>
+ <params>
+ <param>
+ <value>
+ <string>Hello, world!</string>
+ </value>
+ </param>
+ </params>
+ </methodResponse>`)
+ faultWithCode0 := []byte(`<?xml version='1.0'?>
+ <methodResponse>
+ <fault>
+ <value>
+ <struct>
+ <member>
+ <name>faultCode</name>
+ <value>
+ <int>0</int>
+ </value>
+ </member>
+ <member>
+ <name>faultString</name>
+ <value>
+ <string>Bad XML request!</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </fault>
+ </methodResponse>`)
+ faultWithUnexpectedMember := []byte(`<?xml version='1.0'?>
+ <methodResponse>
+ <fault>
+ <value>
+ <struct>
+ <member>
+ <name>faultCode</name>
+ <value>
+ <int>1</int>
+ </value>
+ </member>
+ <member>
+ <name>faultString</name>
+ <value>
+ <string>Bad XML request!</string>
+ </value>
+ </member>
+ <member>
+ <name>unexpectedMember</name>
+ <value>
+ <string>Foo</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </fault>
+ </methodResponse>`)
+ type testCase struct {
+ b []byte
+ expectErr bool
+ expectFault bool
+ expectedFaultCode int
+ expectedFaultString string
+ }
+ for i, tc := range []testCase{
+ {hasFault, true, true, 1, "Bad XML request!"},
+ {noFault, false, false, 0, ""},
+ {faultWithCode0, true, false, 0, ""},
+ {faultWithUnexpectedMember, true, false, 0, ""},
+ } {
+ // Check the XML bytes for fault.
+ r := methodResponse{}
+ if err := xml.Unmarshal(tc.b, &r); err != nil {
+ t.Errorf("tc #%d: failed to unmarshal bytes: %v", i, err)
+ continue
+ }
+ e := r.checkFault()
+
+ // Check whether we got an error if expected.
+ // If no error is expected, then continue early, since the rest of the test operates on the error.
+ if !tc.expectErr {
+ if e != nil {
+ t.Errorf("tc #%d: unexpected checkFault error response: got %v; want nil", i, e)
+ }
+ continue
+ } else if e == nil {
+ t.Errorf("tc #%d: unexpected checkFault error response: got nil; want error", i)
+ continue
+ }
+
+ // Check whether we got a fault if expected.
+ // If no fault is expected, then continue early, since the rest of the test operates on the error.
+ fe, isFault := e.(FaultError)
+ if !tc.expectFault {
+ if isFault {
+ t.Errorf("tc #%d: unexpected checkFault error: got '%v'; want non-Fault error", i, e)
+ }
+ continue
+ } else if !isFault {
+ t.Errorf("tc #%d: unexpected checkFault error: got '%v'; want FaultError", i, e)
+ }
+
+ // Check the Fault's attributes
+ if fe.Code != tc.expectedFaultCode {
+ t.Errorf("tc #%d: checkFault returned unexpected code: got %d; want %d", i, fe.Code, tc.expectedFaultCode)
+ }
+ if fe.Reason != tc.expectedFaultString {
+ t.Errorf("tc #%d: checkFault returned unexpected reason: got %s; want %s", i, fe.Reason, tc.expectedFaultString)
+ }
+ }
+}
diff --git a/src/chromiumos/test/servod/docker/build-dockerimage.sh b/src/chromiumos/test/servod/docker/build-dockerimage.sh
index 90a9989..aaa199e 100755
--- a/src/chromiumos/test/servod/docker/build-dockerimage.sh
+++ b/src/chromiumos/test/servod/docker/build-dockerimage.sh
@@ -69,7 +69,7 @@
done
build_server_image \
- --service "cros-servod" \
+ --service "cros-servod" \
--docker_file "${script_dir}/Dockerfile" \
--chroot "${chroot}" \
--tags "${tags}" \