[autotest] Support provisioning multiple duts connected to a testbed
This is the first step to support provisioning multiple duts connected to a
testbed. The image string entered in afe create job tab should specify the
serial of each build to install, e.g.,
branch1/shamu-userdebug/LATEST:serial1,branch2/shamu-userdebug/LATEST:serial2
BUG=chromium:574173
TEST=local run test
http://dshi.mtv/afe/#tab_id=view_job&object_id=3251
Change-Id: I10d357a34a024ca2b2d2058397f3e2927aa23c70
Reviewed-on: https://chromium-review.googlesource.com/320591
Commit-Ready: Dan Shi <dshi@google.com>
Tested-by: Dan Shi <dshi@google.com>
Reviewed-by: Simran Basi <sbasi@chromium.org>
Reviewed-by: Kevin Cheng <kevcheng@chromium.org>
diff --git a/server/control_segments/install b/server/control_segments/install
index 20d6944..ab08e3d 100644
--- a/server/control_segments/install
+++ b/server/control_segments/install
@@ -1,5 +1,6 @@
def install(machine):
- host = hosts.create_host(machine, initialize=False, auto_monitor=False)
+ host = hosts.create_target_machine(machine, initialize=False,
+ auto_monitor=False)
host.machine_install()
diff --git a/server/hosts/adb_host.py b/server/hosts/adb_host.py
index b72b86c..828bf08 100644
--- a/server/hosts/adb_host.py
+++ b/server/hosts/adb_host.py
@@ -58,7 +58,7 @@
# Default maximum number of seconds to wait for a device to be up.
DEFAULT_WAIT_UP_TIME_SECONDS = 300
# Maximum number of seconds to wait for a device to be up after it's wiped.
-WAIT_UP_AFTER_WIPE_TIME_SECONDS = 900
+WAIT_UP_AFTER_WIPE_TIME_SECONDS = 1200
OS_TYPE_ANDROID = 'android'
OS_TYPE_BRILLO = 'brillo'
@@ -184,18 +184,18 @@
# TODO (sbasi/kevcheng): Once the teststation host is committed,
# refactor the serial retrieval.
adb_serial = adb_serial or self.host_attributes.get('serials', None)
- self._adb_serial = adb_serial
- self._fastboot_serial = fastboot_serial or adb_serial
+ self.adb_serial = adb_serial
+ self.fastboot_serial = fastboot_serial or adb_serial
self.teststation = (teststation if teststation
else teststation_host.create_teststationhost(hostname=hostname))
msg ='Initializing ADB device on host: %s' % hostname
if self._device_hostname:
msg += ', device hostname: %s' % self._device_hostname
- if self._adb_serial:
- msg += ', ADB serial: %s' % self._adb_serial
- if self._fastboot_serial:
- msg += ', fastboot serial: %s' % self._fastboot_serial
+ if self.adb_serial:
+ msg += ', ADB serial: %s' % self.adb_serial
+ if self.fastboot_serial:
+ msg += ', fastboot serial: %s' % self.fastboot_serial
logging.debug(msg)
self._reset_adbd_connection()
@@ -207,7 +207,7 @@
if not self._device_hostname:
return
logging.debug('Connecting to device over TCP/IP')
- if self._device_hostname == self._adb_serial:
+ if self._device_hostname == self.adb_serial:
# We previously had a connection to this device, restart the ADB
# server.
self.adb_run('kill-server')
@@ -298,9 +298,9 @@
@returns a CMDResult object.
"""
if function == ADB_CMD:
- serial = self._adb_serial
+ serial = self.adb_serial
elif function == FASTBOOT_CMD:
- serial = self._fastboot_serial
+ serial = self.fastboot_serial
else:
raise NotImplementedError('Mode %s is not supported' % function)
@@ -536,7 +536,7 @@
"""Get a list of devices currently attached to the test station and
accessible with the adb command."""
devices = self._get_devices(use_adb=True)
- if self._adb_serial is None and len(devices) > 1:
+ if self.adb_serial is None and len(devices) > 1:
raise error.AutoservError(
'Not given ADB serial but multiple devices detected')
return devices
@@ -547,7 +547,7 @@
accessible by fastboot command.
"""
devices = self._get_devices(use_adb=False)
- if self._fastboot_serial is None and len(devices) > 1:
+ if self.fastboot_serial is None and len(devices) > 1:
raise error.AutoservError(
'Not given fastboot serial but multiple devices detected')
return devices
@@ -567,7 +567,7 @@
"""
if command == ADB_CMD:
devices = self.adb_devices()
- serial = self._adb_serial
+ serial = self.adb_serial
# ADB has a device state, if the device is not online, no
# subsequent ADB command will complete.
if len(devices) == 0 or not self.is_device_ready():
@@ -575,7 +575,7 @@
return False
elif command == FASTBOOT_CMD:
devices = self.fastboot_devices()
- serial = self._fastboot_serial
+ serial = self.fastboot_serial
else:
raise NotImplementedError('Mode %s is not supported' % command)
@@ -1049,7 +1049,7 @@
raise
- def _stage_build_for_install(self, build_name):
+ def stage_build_for_install(self, build_name):
"""Stage a build on a devserver and return the build_url and devserver.
@param build_name: a name like git-master/shamu-userdebug/2040953
@@ -1097,7 +1097,7 @@
delete_build_folder = bool(not build_local_path)
if not build_url and self._parser.options.image:
- build_url, _ = self._stage_build_for_install(
+ build_url, _ = self.stage_build_for_install(
self._parser.options.image)
try:
@@ -1162,7 +1162,7 @@
delete_build_folder = bool(not build_local_path)
if not build_url and self._parser.options.image:
- build_url, _ = self._stage_build_for_install(
+ build_url, _ = self.stage_build_for_install(
self._parser.options.image)
try:
diff --git a/server/hosts/testbed.py b/server/hosts/testbed.py
index da82134..7b281ea 100644
--- a/server/hosts/testbed.py
+++ b/server/hosts/testbed.py
@@ -5,18 +5,31 @@
"""This class defines the TestBed class."""
import logging
+import re
+from multiprocessing import pool
import common
+from autotest_lib.client.common_lib import error
from autotest_lib.server.cros.dynamic_suite import constants
from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
+from autotest_lib.server import autoserv_parser
from autotest_lib.server.hosts import adb_host
from autotest_lib.server.hosts import teststation_host
+# Thread pool size to provision multiple devices in parallel.
+_POOL_SIZE = 4
+
+# Pattern for the image name when used to provision a dut connected to testbed.
+# It should follow the naming convention of branch/target/build_id[:serial],
+# where serial is optional.
+_IMAGE_NAME_PATTERN = '(.*/.*/.*)(?::(.*))'
+
class TestBed(object):
"""This class represents a collection of connected teststations and duts."""
+ _parser = autoserv_parser.autoserv_parser
def __init__(self, hostname='localhost', host_attributes={},
adb_serials=None, **dargs):
@@ -151,3 +164,71 @@
for adb_device in self.get_adb_devices().values():
adb_device.cleanup()
+
+ def _parse_image(self, image_string):
+ """Parse the image string to a dictionary.
+
+ Sample value of image_string:
+ branch1/shamu-userdebug/LATEST:ZX1G2,branch2/shamu-userdebug/LATEST
+
+ @param image_string: A comma separated string of images. The image name
+ is in the format of branch/target/build_id[:serial]. Serial is
+ optional once testbed machine_install supports allocating DUT
+ based on board.
+
+ @returns: A list of tuples of (build, serial). serial could be None if
+ it's not specified.
+ """
+ images = []
+ for image in image_string.split(','):
+ match = re.match(_IMAGE_NAME_PATTERN, image)
+ if not match:
+ raise error.InstallError(
+ 'Image name of "%s" has invalid format. It should '
+ 'follow naming convention of '
+ 'branch/target/build_id[:serial]', image)
+ serial = None if len(match.groups()) == 1 else match.group(2)
+ images.append((match.group(1), serial))
+ return images
+
+
+ @staticmethod
+ def _install_device(inputs):
+ """Install build to a device with the given inputs.
+
+ @param inputs: A dictionary of the arguments needed to install a device.
+ Keys include:
+ host: An ADBHost object of the device.
+ build_url: Devserver URL to the build to install.
+ """
+ host = inputs['host']
+ build_url = inputs['build_url']
+
+ logging.info('Starting installing device %s:%s from build url %s',
+ host.hostname, host.adb_serial, build_url)
+ host.machine_install(build_url=build_url)
+ logging.info('Finished installing device %s:%s from build url %s',
+ host.hostname, host.adb_serial, build_url)
+
+
+ def machine_install(self):
+ """Install the DUT."""
+ if not self._parser.options.image:
+ raise error.InstallError('No image string is provided to test bed.')
+ images = self._parse_image(self._parser.options.image)
+
+ arguments = []
+ for build, serial in images:
+ # TODO(crbug.com/574543): Support allocating DUT based on board, not
+ # serial.
+ if not serial in self.get_adb_devices():
+ raise error.InstallError('Serial "%s" is not found in the '
+ 'devices connected to the test bed')
+ host = self.get_adb_devices()[serial]
+ build_url, _ = host.stage_build_for_install(build)
+ arguments.append({'host': host,
+ 'build_url': build_url})
+
+ thread_pool = pool.ThreadPool(_POOL_SIZE)
+ thread_pool.map(self._install_device, arguments)
+ thread_pool.close()