Merge "Extend ACTS to run against remote testbeds"
diff --git a/acts/framework/acts/controllers/adb.py b/acts/framework/acts/controllers/adb.py
index be3c408..bbd54bf 100644
--- a/acts/framework/acts/controllers/adb.py
+++ b/acts/framework/acts/controllers/adb.py
@@ -19,10 +19,11 @@
import logging
import random
import socket
-import subprocess
import time
from acts.controllers.utils_lib import host_utils
+from acts.controllers.utils_lib.ssh import connection
+from acts.libs.proc import job
class AdbError(Exception):
"""Raised when there is an error in adb operations."""
@@ -38,7 +39,7 @@
) % (self.cmd, self.ret_code, self.stdout, self.stderr)
-class AdbProxy():
+class AdbProxy(object):
"""Proxy class for ADB.
For syntactic reasons, the '-' in adb commands need to be replaced with
@@ -48,12 +49,41 @@
>> adb.devices() # will return the console output of "adb devices".
"""
- def __init__(self, serial=""):
+ _SERVER_LOCAL_PORT = None
+
+ def __init__(self, serial="", ssh_connection=None):
+ """Construct an instance of AdbProxy.
+
+ Args:
+ serial: str serial number of Android device from `adb devices`
+ ssh_connection: SshConnection instance if the Android device is
+ conected to a remote host that we can reach via SSH.
+ """
self.serial = serial
+ adb_path = str(self._exec_cmd("which adb"), "utf-8").strip()
+ adb_cmd = [adb_path]
if serial:
- self.adb_str = "adb -s {}".format(serial)
- else:
- self.adb_str = "adb"
+ adb_cmd.append("-s %s" % serial)
+ if ssh_connection is not None and not AdbProxy._SERVER_LOCAL_PORT:
+ # Kill all existing adb processes on the remote host (if any)
+ # Note that if there are none, then pkill exits with non-zero status
+ ssh_connection.run("pkill adb", ignore_status=True)
+ # Copy over the adb binary to a temp dir
+ temp_dir = ssh_connection.run("mktemp -d").stdout.strip()
+ ssh_connection.send_file(adb_path, temp_dir)
+ # Start up a new adb server running as root from the copied binary.
+ remote_adb_cmd = "%s/adb %s root" % (
+ temp_dir,
+ "-s %s" % serial if serial else "")
+ ssh_connection.run(remote_adb_cmd)
+ # Proxy a local port to the adb server port
+ local_port = ssh_connection.create_ssh_tunnel(5037)
+ AdbProxy._SERVER_LOCAL_PORT = local_port
+
+ if AdbProxy._SERVER_LOCAL_PORT:
+ adb_cmd.append("-P %d" % local_port)
+ self.adb_cmd = " ".join(adb_cmd)
+ self._ssh_connection = ssh_connection
def _exec_cmd(self, cmd):
"""Executes adb commands in a new shell.
@@ -70,12 +100,9 @@
Raises:
AdbError is raised if the adb command exit code is not 0.
"""
- proc = subprocess.Popen(cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- shell=True)
- (out, err) = proc.communicate()
- ret = proc.returncode
+ result = job.run(cmd, ignore_status=True)
+ ret, out, err = result.exit_status, result.raw_stdout, result.raw_stderr
+
logging.debug("cmd: %s, stdout: %s, stderr: %s, ret: %s", cmd, out,
err, ret)
if ret == 0:
@@ -93,6 +120,10 @@
host_port: Port number to use on the computer.
device_port: Port number to use on the android device.
"""
+ if self._ssh_connection:
+ # TODO(wiley): We need to actually pick a free port on the
+ # intermediate host
+ self._ssh_connection.create_ssh_tunnel(host_port, local_port=host_port)
self.forward("tcp:{} tcp:{}".format(host_port, device_port))
def getprop(self, prop_name):
diff --git a/acts/framework/acts/controllers/android_device.py b/acts/framework/acts/controllers/android_device.py
index 94d2017..a7e0d08 100644
--- a/acts/framework/acts/controllers/android_device.py
+++ b/acts/framework/acts/controllers/android_device.py
@@ -30,6 +30,8 @@
from acts.controllers import fastboot
from acts.controllers import sl4a_client
from acts.controllers.utils_lib import host_utils
+from acts.controllers.utils_lib.ssh import connection
+from acts.controllers.utils_lib.ssh import settings
ACTS_CONTROLLER_CONFIG_NAME = "AndroidDevice"
ACTS_CONTROLLER_REFERENCE_NAME = "android_devices"
@@ -72,10 +74,9 @@
else:
# Configs is a list of dicts.
ads = get_instances_with_configs(configs)
- connected_ads = list_adb_devices()
for ad in ads:
- if ad.serial not in connected_ads:
+ if not ad.is_connected():
raise DoesNotExistError(("Android device %s is specified in config"
" but is not attached.") % ad.serial)
_start_services_on_ads(ads)
@@ -209,7 +210,12 @@
raise AndroidDeviceError(
"Required value 'serial' is missing in AndroidDevice config %s."
% c)
- ad = AndroidDevice(serial)
+ ssh_config = c.pop("ssh_config", None)
+ ssh_connection = None
+ if ssh_config is not None:
+ ssh_settings = settings.from_config(ssh_config)
+ ssh_connection = connection.SshConnection(ssh_settings)
+ ad = AndroidDevice(serial, ssh_connection=ssh_connection)
ad.load_config(c)
results.append(ad)
return results
@@ -338,7 +344,8 @@
via fastboot.
"""
- def __init__(self, serial="", host_port=None, device_port=8080):
+ def __init__(self, serial="", host_port=None, device_port=8080,
+ ssh_connection=None):
self.serial = serial
self.h_port = host_port
self.d_port = device_port
@@ -352,10 +359,12 @@
self._event_dispatchers = {}
self.adb_logcat_process = None
self.adb_logcat_file_path = None
- self.adb = adb.AdbProxy(serial)
- self.fastboot = fastboot.FastbootProxy(serial)
+ self.adb = adb.AdbProxy(serial, ssh_connection=ssh_connection)
+ self.fastboot = fastboot.FastbootProxy(
+ serial, ssh_connection=ssh_connection)
if not self.is_bootloader:
self.root_adb()
+ self._ssh_connection = ssh_connection
def clean_up(self):
"""Cleans up the AndroidDevice object and releases any resources it
@@ -364,6 +373,9 @@
self.stop_services()
if self.h_port:
self.adb.forward("--remove tcp:%d" % self.h_port)
+ if self._ssh_connection:
+ self._ssh_connection.close()
+
# TODO(angli): This function shall be refactored to accommodate all services
# and not have hard coded switch for SL4A when b/29157104 is done.
@@ -399,6 +411,11 @@
self.terminate_all_sessions()
sl4a_client.stop_sl4a(self.adb)
+ def is_connected(self):
+ out = self.adb.devices()
+ devices = _parse_device_list(out, "device")
+ return self.serial in devices
+
@property
def build_info(self):
"""Get the build info of this Android device, including build id and
diff --git a/acts/framework/acts/controllers/fastboot.py b/acts/framework/acts/controllers/fastboot.py
index b06e3f3..7cfbcbb 100644
--- a/acts/framework/acts/controllers/fastboot.py
+++ b/acts/framework/acts/controllers/fastboot.py
@@ -53,15 +53,19 @@
>> fb.devices() # will return the console output of "fastboot devices".
"""
- def __init__(self, serial=""):
+ def __init__(self, serial="", ssh_connection=None):
self.serial = serial
if serial:
self.fastboot_str = "fastboot -s {}".format(serial)
else:
self.fastboot_str = "fastboot"
+ self.ssh_connection = ssh_connection
def _exec_fastboot_cmd(self, name, arg_str):
- return exe_cmd(' '.join((self.fastboot_str, name, arg_str)))
+ command = ' '.join((self.fastboot_str, name, arg_str))
+ if self.ssh_connection:
+ return self.connection.run(command).raw_stdout
+ return exe_cmd(command)
def args(self, *args):
return exe_cmd(' '.join((self.fastboot_str, ) + args))
diff --git a/acts/framework/acts/controllers/utils_lib/ssh/connection.py b/acts/framework/acts/controllers/utils_lib/ssh/connection.py
index b56d43e..3295d2a 100644
--- a/acts/framework/acts/controllers/utils_lib/ssh/connection.py
+++ b/acts/framework/acts/controllers/utils_lib/ssh/connection.py
@@ -333,3 +333,14 @@
local_port, port, tunnel_proc.pid)
self._tunnels.append(tunnel_proc)
return local_port
+
+ def send_file(self, local_path, remote_path):
+ """Send a file from the local host to the remote host.
+
+ Args:
+ local_path: string path of file to send on local host.
+ remote_path: string path to copy file to on remote host.
+ """
+ # TODO: This may belong somewhere else: b/32572515
+ user_host = self._formatter.format_host_name(self._settings)
+ job.run('scp %s %s:%s' % (local_path, user_host, remote_path))
diff --git a/acts/framework/acts/controllers/utils_lib/ssh/settings.py b/acts/framework/acts/controllers/utils_lib/ssh/settings.py
index 97f94b6..b206db6 100644
--- a/acts/framework/acts/controllers/utils_lib/ssh/settings.py
+++ b/acts/framework/acts/controllers/utils_lib/ssh/settings.py
@@ -13,6 +13,28 @@
# limitations under the License.
+"""Create a SshSettings from a dictionary from an ACTS config
+
+Args:
+ config dict instance from an ACTS config
+
+Returns:
+ An instance of SshSettings or None
+"""
+def from_config(config):
+ if config is None:
+ return None # Having no settings is not an error
+
+ user = config.get('user', None)
+ host = config.get('host', None)
+ port = config.get('port', 22)
+ if user is None or host is None:
+ raise ValueError('Malformed SSH config did not include user and '
+ 'host keys: %s' % config)
+
+ return SshSettings(host, user, port=port)
+
+
class SshSettings(object):
"""Contains settings for ssh.
diff --git a/acts/framework/tests/acts_android_device_test.py b/acts/framework/tests/acts_android_device_test.py
index d91e719..64123b9 100755
--- a/acts/framework/tests/acts_android_device_test.py
+++ b/acts/framework/tests/acts_android_device_test.py
@@ -95,6 +95,9 @@
elif params == "sys.boot_completed":
return "1"
+ def devices(self):
+ return bytearray("\t".join([str(self.serial), "device"]), "utf-8")
+
def bugreport(self, params):
expected = os.path.join(logging.log_path,
"AndroidDevice%s" % self.serial, "BugReports",