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: &params})
+}
+
+// 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: &params}
+	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}"                           \