[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()