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}" \