[autotest] Support run_suite with suite package and SSP for Brillo.

This change allows one to run run_suite command without using
--run_prod_code for Brillo build.
create_suite_job will use test_suites and control_files packages from
the Brillo build to create suite job and its test jobs.

Server-side packaging is also supported for newer builds that have
autotest_server_package artifact build.

BUG=chromium:584705
TEST=run in local instance, unittest, verify in moblab

Change-Id: Ia96ca4de919b178302580c23f911bb6445016285
Reviewed-on: https://chromium-review.googlesource.com/332431
Commit-Ready: Dan Shi <dshi@google.com>
Tested-by: Dan Shi <dshi@google.com>
Reviewed-by: Dan Shi <dshi@google.com>
diff --git a/client/common_lib/cros/dev_server.py b/client/common_lib/cros/dev_server.py
index 489ccdd..d5a2fd4 100644
--- a/client/common_lib/cros/dev_server.py
+++ b/client/common_lib/cros/dev_server.py
@@ -1085,6 +1085,41 @@
             raise DevServerException(error_message)
 
 
+    @remote_devserver_call()
+    def list_control_files(self, build, suite_name=''):
+        """Ask the devserver to list all control files for |build|.
+
+        @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514)
+                      whose control files the caller wants listed.
+        @param suite_name: The name of the suite for which we require control
+                           files.
+        @return None on failure, or a list of control file paths
+                (e.g. server/site_tests/autoupdate/control)
+        @raise DevServerException upon any return code that's not HTTP OK.
+        """
+        build = self.translate(build)
+        call = self.build_call('controlfiles', build=build,
+                               suite_name=suite_name)
+        return self.run_call(call, readline=True)
+
+
+    @remote_devserver_call()
+    def get_control_file(self, build, control_path):
+        """Ask the devserver for the contents of a control file.
+
+        @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514)
+                      whose control file the caller wants to fetch.
+        @param control_path: The file to fetch
+                             (e.g. server/site_tests/autoupdate/control)
+        @return The contents of the desired file.
+        @raise DevServerException upon any return code that's not HTTP OK.
+        """
+        build = self.translate(build)
+        call = self.build_call('controlfiles', build=build,
+                               control_path=control_path)
+        return self.run_call(call)
+
+
 class ImageServer(ImageServerBase):
     """Class for DevServer that handles RPCs related to CrOS images.
 
@@ -1157,7 +1192,7 @@
 
 
     @remote_devserver_call()
-    def stage_artifacts(self, image, artifacts=None, files='',
+    def stage_artifacts(self, image=None, artifacts=None, files='',
                         archive_url=None):
         """Tell the devserver to download and stage |artifacts| from |image|.
 
@@ -1307,41 +1342,6 @@
 
 
     @remote_devserver_call()
-    def list_control_files(self, build, suite_name=''):
-        """Ask the devserver to list all control files for |build|.
-
-        @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514)
-                      whose control files the caller wants listed.
-        @param suite_name: The name of the suite for which we require control
-                           files.
-        @return None on failure, or a list of control file paths
-                (e.g. server/site_tests/autoupdate/control)
-        @raise DevServerException upon any return code that's not HTTP OK.
-        """
-        build = self.translate(build)
-        call = self.build_call('controlfiles', build=build,
-                               suite_name=suite_name)
-        return self.run_call(call, readline=True)
-
-
-    @remote_devserver_call()
-    def get_control_file(self, build, control_path):
-        """Ask the devserver for the contents of a control file.
-
-        @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514)
-                      whose control file the caller wants to fetch.
-        @param control_path: The file to fetch
-                             (e.g. server/site_tests/autoupdate/control)
-        @return The contents of the desired file.
-        @raise DevServerException upon any return code that's not HTTP OK.
-        """
-        build = self.translate(build)
-        call = self.build_call('controlfiles', build=build,
-                               control_path=control_path)
-        return self.run_call(call)
-
-
-    @remote_devserver_call()
     def get_dependencies_file(self, build):
         """Ask the dev server for the contents of the suite dependencies file.
 
@@ -1497,8 +1497,8 @@
 
 
     @remote_devserver_call()
-    def stage_artifacts(self, target, build_id, branch, artifacts=None,
-                        files='', archive_url=None):
+    def stage_artifacts(self, target=None, build_id=None, branch=None,
+                        image=None, artifacts=None, files='', archive_url=None):
         """Tell the devserver to download and stage |artifacts| from |image|.
 
          This is the main call point for staging any specific artifacts for a
@@ -1512,6 +1512,8 @@
                                shamu-userdebug.
         @param build_id: Build id of the android build to stage.
         @param branch: Branch of the android build to stage.
+        @param image: Name of a build to test, in the format of
+                      branch/target/build_id
         @param artifacts: A list of artifacts.
         @param files: A list of files to stage.
         @param archive_url: Optional parameter that has the archive_url to stage
@@ -1520,6 +1522,12 @@
 
         @raise DevServerException upon any return code that's not HTTP OK.
         """
+        if image and not target and not build_id and not branch:
+            branch, target, build_id = utils.parse_launch_control_build(image)
+        if not target or not build_id or not branch:
+            raise DevServerException('Must specify all build info (target, '
+                                     'build_id and branch) to stage.')
+
         android_build_info = {'target': target,
                               'build_id': build_id,
                               'branch': branch}
@@ -1625,7 +1633,7 @@
 
         @return The actual build name to use.
         """
-        branch, target, build_id = utils.parse_android_build(build_name)
+        branch, target, build_id = utils.parse_launch_control_build(build_name)
         if build_id != 'LATEST':
             return build_name
         call = self.build_call('latestbuild', branch=branch, target=target,
@@ -1722,3 +1730,21 @@
         return None
     loads = sorted(loads, cmp=_compare_load)
     return loads[0]['devserver']
+
+
+def resolve(build, hostname=None):
+    """Resolve a devserver can be used for given build and hostname.
+
+    @param build: Name of a build to stage on devserver, e.g.,
+                  ChromeOS build: daisy-release/R50-1234.0.0
+                  Launch Control build: git_mnc_release/shamu-eng
+    @param hostname: Hostname of a devserver for, default is None, which means
+            devserver is not restricted by the network location of the host.
+
+    @return: A DevServer instance that can be used to stage given build for the
+             given host.
+    """
+    if utils.is_launch_control_build(build):
+        return AndroidBuildServer.resolve(build, hostname)
+    else:
+        return ImageServer.resolve(build, hostname)
diff --git a/client/common_lib/site_utils.py b/client/common_lib/site_utils.py
index 6541c55..f269597 100644
--- a/client/common_lib/site_utils.py
+++ b/client/common_lib/site_utils.py
@@ -699,10 +699,10 @@
     return default_ssid
 
 
-def parse_android_build(build_name):
-    """Get branch, target, build_id from the given build_name.
+def parse_launch_control_build(build_name):
+    """Get branch, target, build_id from the given Launch Control build_name.
 
-    @param build_name: Name of an Android build, should be formated as
+    @param build_name: Name of a Launch Control build, should be formated as
                        branch/target/build_id
 
     @return: Tuple of branch, target, build_id
@@ -712,6 +712,18 @@
     return branch, target, build_id
 
 
+def parse_android_target(target):
+    """Get board and build type from the given target.
+
+    @param target: Name of an Android build target, e.g., shamu-eng.
+
+    @return: Tuple of board, build_type
+    @raise ValueError: If the target is not correctly formated.
+    """
+    board, build_type = target.split('-')
+    return board, build_type
+
+
 def parse_android_board_label(board_label_name):
     """Parse the os_type and board name from board label name of an
     Android/Brillo board.
@@ -735,23 +747,44 @@
 
 
 def parse_launch_control_target(target):
-    """Parse the board name and target type from a Launch Control target.
+    """Parse the build target and type from a Launch Control target.
 
-    The Launch Control target has the format of board-target_type, e.g.,
-    shamu-eng or dragonboard-userdebug. This method extracts the board name
-    and target type from the target name.
+    The Launch Control target has the format of build_target-build_type, e.g.,
+    shamu-eng or dragonboard-userdebug. This method extracts the build target
+    and type from the target name.
 
     @param target: Name of a Launch Control target, e.g., shamu-eng.
 
-    @return: (board, target_type), e.g., ('shamu', 'userdebug')
+    @return: (build_target, build_type), e.g., ('shamu', 'userdebug')
     """
-    match = re.match('(?P<board>.+)-(?P<target_type>[^-]+)', target)
+    match = re.match('(?P<build_target>.+)-(?P<build_type>[^-]+)', target)
     if match:
-        return match.group('board'), match.group('target_type')
+        return match.group('build_target'), match.group('build_type')
     else:
         return None, None
 
 
+def is_launch_control_build(build):
+    """Check if a given build is a Launch Control build.
+
+    @param build: Name of a build, e.g.,
+                  ChromeOS build: daisy-release/R50-1234.0.0
+                  Launch Control build: git_mnc_release/shamu-eng
+
+    @return: True if the build name matches the pattern of a Launch Control
+             build, False otherwise.
+    """
+    try:
+        _, target, _ = parse_launch_control_build(build)
+        build_target, _ = parse_launch_control_target(target)
+        if build_target:
+            return True
+    except ValueError:
+        # parse_launch_control_build or parse_launch_control_target failed.
+        pass
+    return False
+
+
 def which(exec_file):
     """Finds an executable file.
 
diff --git a/frontend/afe/site_rpc_interface.py b/frontend/afe/site_rpc_interface.py
index ac7e9a4..e709ba7 100644
--- a/frontend/afe/site_rpc_interface.py
+++ b/frontend/afe/site_rpc_interface.py
@@ -107,13 +107,13 @@
     # Ensure components of |build| necessary for installing images are staged
     # on the dev server. However set synchronous to False to allow other
     # components to be downloaded in the background.
-    ds = dev_server.ImageServer.resolve(build)
+    ds = dev_server.resolve(build)
     timings[constants.DOWNLOAD_STARTED_TIME] = formatted_now()
     timer = autotest_stats.Timer('control_files.stage.%s' % (
             ds.get_server_name(ds.url()).replace('.', '_')))
     try:
         with timer:
-            ds.stage_artifacts(build, ['test_suites'])
+            ds.stage_artifacts(image=build, artifacts=['test_suites'])
     except dev_server.DevServerException as e:
         raise error.StageControlFileFailure(
                 "Failed to stage %s: %s" % (build, e))
@@ -209,7 +209,7 @@
 
     suite_name = canonicalize_suite_name(name)
     if run_prod_code:
-        ds = dev_server.ImageServer.resolve(build)
+        ds = dev_server.resolve(build)
         keyvals = {}
         getter = control_file_getter.FileSystemGetter(
                 [_CONFIG.get_config_value('SCHEDULER',
@@ -641,7 +641,7 @@
         raise ValueError('Could not resolve build %s: %s' % (build, e))
 
     try:
-        ds.stage_artifacts(build, ['test_suites'])
+        ds.stage_artifacts(image=build, artifacts=['test_suites'])
     except dev_server.DevServerException as e:
         raise error.StageControlFileFailure(
                 'Failed to stage %s: %s' % (build, e))
diff --git a/frontend/afe/site_rpc_interface_unittest.py b/frontend/afe/site_rpc_interface_unittest.py
index 0606dbe..db4a300 100755
--- a/frontend/afe/site_rpc_interface_unittest.py
+++ b/frontend/afe/site_rpc_interface_unittest.py
@@ -64,7 +64,7 @@
 
     def _setupDevserver(self):
         self.mox.StubOutClassWithMocks(dev_server, 'ImageServer')
-        dev_server.ImageServer.resolve(self._BUILD).AndReturn(self.dev_server)
+        dev_server.resolve(self._BUILD).AndReturn(self.dev_server)
 
 
     def _mockDevServerGetter(self, get_control_file=True):
@@ -114,7 +114,7 @@
         self.dev_server.url().AndReturn('mox_url')
         self.dev_server.get_server_name(mox.IgnoreArg()).AndReturn('mox_url')
         self.dev_server.stage_artifacts(
-            self._BUILD, ['test_suites']).AndRaise(
+                image=self._BUILD, artifacts=['test_suites']).AndRaise(
                 dev_server.DevServerException())
         self.mox.ReplayAll()
         self.assertRaises(error.StageControlFileFailure,
@@ -131,8 +131,8 @@
 
         self.dev_server.url().AndReturn('mox_url')
         self.dev_server.get_server_name(mox.IgnoreArg()).AndReturn('mox_url')
-        self.dev_server.stage_artifacts(self._BUILD,
-                                        ['test_suites']).AndReturn(True)
+        self.dev_server.stage_artifacts(
+                image=self._BUILD, artifacts=['test_suites']).AndReturn(True)
 
         self.dev_server.url().AndReturn('mox_url')
         self.dev_server.get_server_name(mox.IgnoreArg()).AndReturn('mox_url')
@@ -153,8 +153,8 @@
 
         self.dev_server.url().AndReturn('mox_url')
         self.dev_server.get_server_name(mox.IgnoreArg()).AndReturn('mox_url')
-        self.dev_server.stage_artifacts(self._BUILD,
-                                        ['test_suites']).AndReturn(True)
+        self.dev_server.stage_artifacts(
+                image=self._BUILD, artifacts=['test_suites']).AndReturn(True)
 
         self.dev_server.url().AndReturn('mox_url')
         self.dev_server.get_server_name(mox.IgnoreArg()).AndReturn('mox_url')
@@ -201,8 +201,8 @@
 
         self.dev_server.url().AndReturn('mox_url')
         self.dev_server.get_server_name(mox.IgnoreArg()).AndReturn('mox_url')
-        self.dev_server.stage_artifacts(self._BUILD,
-                                        ['test_suites']).AndReturn(True)
+        self.dev_server.stage_artifacts(
+                image=self._BUILD, artifacts=['test_suites']).AndReturn(True)
 
         self.dev_server.url().AndReturn('mox_url')
         self.dev_server.get_server_name(mox.IgnoreArg()).AndReturn('mox_url')
@@ -225,8 +225,8 @@
 
         self.dev_server.url().AndReturn('mox_url')
         self.dev_server.get_server_name(mox.IgnoreArg()).AndReturn('mox_url')
-        self.dev_server.stage_artifacts(self._BUILD,
-                                        ['test_suites']).AndReturn(True)
+        self.dev_server.stage_artifacts(
+                image=self._BUILD, artifacts=['test_suites']).AndReturn(True)
 
         self.dev_server.url().AndReturn('mox_url')
         self.dev_server.get_server_name(mox.IgnoreArg()).AndReturn('mox_url')
@@ -251,8 +251,8 @@
 
         self.dev_server.url().AndReturn('mox_url')
         self.dev_server.get_server_name(mox.IgnoreArg()).AndReturn('mox_url')
-        self.dev_server.stage_artifacts(self._BUILD,
-                                        ['test_suites']).AndReturn(True)
+        self.dev_server.stage_artifacts(
+                image=self._BUILD, artifacts=['test_suites']).AndReturn(True)
 
         self.dev_server.url().AndReturn('mox_url')
         self.dev_server.get_server_name(mox.IgnoreArg()).AndReturn('mox_url')
@@ -276,8 +276,8 @@
 
         self.dev_server.url().AndReturn('mox_url')
         self.dev_server.get_server_name(mox.IgnoreArg()).AndReturn('mox_url')
-        self.dev_server.stage_artifacts(self._BUILD,
-                                        ['test_suites']).AndReturn(True)
+        self.dev_server.stage_artifacts(
+                image=self._BUILD, artifacts=['test_suites']).AndReturn(True)
 
         self.dev_server.url().AndReturn('mox_url')
         self.dev_server.get_server_name(mox.IgnoreArg()).AndReturn('mox_url')
@@ -304,8 +304,8 @@
 
         self.dev_server.url().AndReturn('mox_url')
         self.dev_server.get_server_name(mox.IgnoreArg()).AndReturn('mox_url')
-        self.dev_server.stage_artifacts(self._BUILD,
-                                        ['test_suites']).AndReturn(True)
+        self.dev_server.stage_artifacts(
+                image=self._BUILD, artifacts=['test_suites']).AndReturn(True)
         self.dev_server.url().AndReturn('mox_url')
         job_id = 5
         self._mockRpcUtils(job_id)
diff --git a/global_config.ini b/global_config.ini
index ec75011..baf6b47 100644
--- a/global_config.ini
+++ b/global_config.ini
@@ -82,6 +82,7 @@
 # not have server side package built or with Autotest code change to support
 # server-side packaging.
 min_version_support_ssp: 6986
+min_launch_control_build_id_support_ssp: 2675445
 
 # Set to True to allow servod to be started automatically in Moblab.
 auto_start_servod: False
@@ -395,3 +396,4 @@
 [ANDROID]
 image_url_pattern: %s/static/%s
 stable_version_dragonboard: git_mnc-brillo-dev/dragonboard-userdebug/2512766
+package_url_pattern: %s/static/%s
diff --git a/server/afe_utils.py b/server/afe_utils.py
index 65c1d46..319bd8a 100644
--- a/server/afe_utils.py
+++ b/server/afe_utils.py
@@ -193,3 +193,11 @@
     add_version_label(host, image_name)
     for attribute, value in host_attributes.items():
         update_host_attribute(host, attribute, value)
+
+
+def get_labels(host, prefix):
+    """Get labels of a host with name started with given prefix.
+
+    @param prefix: Prefix of label names.
+    """
+    return AFE.get_labels(name__startswith=prefix, host__hostname=host.hostname)
diff --git a/server/autoserv b/server/autoserv
index 8de1f6d..c4b6a01 100755
--- a/server/autoserv
+++ b/server/autoserv
@@ -672,9 +672,10 @@
         logging.warn(
                 'Autoserv is required to run with server-side packaging. '
                 'However, no server-side package can be found based on '
-                '`--image`, host attribute job_repo_url or host label of '
-                'cros-version. The test will be executed without '
-                'server-side packaging supported.')
+                '`--image`, host attribute job_repo_url or host OS version '
+                'label. It could be that the build to test is older than the '
+                'minimum version that supports server-side packaging. The test '
+                'will be executed without using erver-side packaging.')
 
     if results:
         logging.info("Results placed in %s" % results)
diff --git a/server/cros/dynamic_suite/dynamic_suite.py b/server/cros/dynamic_suite/dynamic_suite.py
index e5fa77a..ad869ec 100644
--- a/server/cros/dynamic_suite/dynamic_suite.py
+++ b/server/cros/dynamic_suite/dynamic_suite.py
@@ -571,8 +571,9 @@
     # files we should schedule.
     try:
         if not spec.run_prod_code:
-            spec.devserver.stage_artifacts(spec.test_source_build,
-                                           ['control_files', 'test_suites'])
+            spec.devserver.stage_artifacts(
+                    image=spec.test_source_build,
+                    artifacts=['control_files', 'test_suites'])
     except dev_server.DevServerException as e:
         # If we can't get the control files, there's nothing to run.
         raise error.AsynchronousBuildFailure(e)
diff --git a/server/cros/dynamic_suite/dynamic_suite_unittest.py b/server/cros/dynamic_suite/dynamic_suite_unittest.py
index df2fe6d..c4e5734 100755
--- a/server/cros/dynamic_suite/dynamic_suite_unittest.py
+++ b/server/cros/dynamic_suite/dynamic_suite_unittest.py
@@ -123,10 +123,11 @@
 
     def testReimageAndSIGTERM(self):
         """Should reimage_and_run that causes a SIGTERM and fails cleanly."""
-        def suicide(*_):
+        def suicide(*_, **__):
             """Send SIGTERM to current process to exit.
 
             @param _: Ignored.
+            @param __: Ignored.
             """
             os.kill(os.getpid(), signal.SIGTERM)
 
@@ -151,8 +152,9 @@
         spec.test_source_build = Suite.get_test_source_build(self._BUILDS)
         spec.devserver = self.mox.CreateMock(dev_server.ImageServer)
         spec.devserver.stage_artifacts(
-                spec.builds[provision.CROS_VERSION_PREFIX],
-                ['control_files', 'test_suites']).WithSideEffects(suicide)
+                image=spec.builds[provision.CROS_VERSION_PREFIX],
+                artifacts=['control_files', 'test_suites']
+                ).WithSideEffects(suicide)
         spec.run_prod_code = False
 
         self.mox.ReplayAll()
diff --git a/server/cros/dynamic_suite/tools.py b/server/cros/dynamic_suite/tools.py
index 6fd5fe6..11507ae 100644
--- a/server/cros/dynamic_suite/tools.py
+++ b/server/cros/dynamic_suite/tools.py
@@ -40,9 +40,17 @@
     return _CONFIG.get_config_value('CROS', 'infrastructure_user', type=str)
 
 
-def package_url_pattern():
-    """Returns package_url_pattern from global_config."""
-    return _CONFIG.get_config_value('CROS', 'package_url_pattern', type=str)
+def package_url_pattern(is_launch_control_build=False):
+    """Returns package_url_pattern from global_config.
+
+    @param is_launch_control_build: True if the package url is for Launch
+            Control build. Default is False.
+    """
+    if is_launch_control_build:
+        return _CONFIG.get_config_value('ANDROID', 'package_url_pattern',
+                                        type=str)
+    else:
+        return _CONFIG.get_config_value('CROS', 'package_url_pattern', type=str)
 
 
 def try_job_timeout_mins():
@@ -62,14 +70,17 @@
     return package_url_pattern() % (devserver_url, build)
 
 
-def get_devserver_build_from_package_url(package_url):
+def get_devserver_build_from_package_url(package_url,
+                                         is_launch_control_build=False):
     """The inverse method of get_package_url.
 
     @param package_url: a string specifying the package url.
+    @param is_launch_control_build: True if the package url is for Launch
+                Control build. Default is False.
 
     @return tuple containing the devserver_url, build.
     """
-    pattern = package_url_pattern()
+    pattern = package_url_pattern(is_launch_control_build)
     re_pattern = pattern.replace('%s', '(\S+)')
 
     devserver_build_tuple = re.search(re_pattern, package_url).groups()
diff --git a/server/hosts/adb_host.py b/server/hosts/adb_host.py
index f260873..e5ddd82 100644
--- a/server/hosts/adb_host.py
+++ b/server/hosts/adb_host.py
@@ -12,12 +12,15 @@
 import common
 
 from autotest_lib.client.common_lib import error
+from autotest_lib.client.common_lib import global_config
 from autotest_lib.client.common_lib.cros import dev_server
 from autotest_lib.client.common_lib.cros import retry
+from autotest_lib.server import afe_utils
 from autotest_lib.server import autoserv_parser
 from autotest_lib.server import constants as server_constants
 from autotest_lib.server import utils
 from autotest_lib.server.cros import provision
+from autotest_lib.server.cros.dynamic_suite import tools
 from autotest_lib.server.cros.dynamic_suite import constants
 from autotest_lib.server.hosts import abstract_ssh
 from autotest_lib.server.hosts import adb_label
@@ -25,6 +28,8 @@
 from autotest_lib.server.hosts import teststation_host
 
 
+CONFIG = global_config.global_config
+
 ADB_CMD = 'adb'
 FASTBOOT_CMD = 'fastboot'
 SHELL_CMD = 'shell'
@@ -67,20 +72,22 @@
 OS_TYPE_BRILLO = 'brillo'
 
 # Regex to parse build name to get the detailed build information.
-BUILD_REGEX = ('(?P<BRANCH>([^/]+))/(?P<BOARD>([^/]+))-'
+BUILD_REGEX = ('(?P<BRANCH>([^/]+))/(?P<BUILD_TARGET>([^/]+))-'
                '(?P<BUILD_TYPE>([^/]+))/(?P<BUILD_ID>([^/]+))')
 # Regex to parse devserver url to get the detailed build information. Sample
 # url: http://$devserver:8080/static/branch/target/build_id
 DEVSERVER_URL_REGEX = '.*/%s/*' % BUILD_REGEX
 
-ANDROID_IMAGE_FILE_FMT = '%(board)s-img-%(build_id)s.zip'
+ANDROID_IMAGE_FILE_FMT = '%(build_target)s-img-%(build_id)s.zip'
 ANDROID_BOOTLOADER = 'bootloader.img'
 ANDROID_RADIO = 'radio.img'
 ANDROID_BOOT = 'boot.img'
 ANDROID_SYSTEM = 'system.img'
 ANDROID_VENDOR = 'vendor.img'
 BRILLO_VENDOR_PARTITIONS_FILE_FMT = (
-        '%(board)s-vendor_partitions-%(build_id)s.zip')
+        '%(build_target)s-vendor_partitions-%(build_id)s.zip')
+AUTOTEST_SERVER_PACKAGE_FILE_FMT = (
+        '%(build_target)s-autotest_server_package-%(build_id)s.tar.bz2')
 
 # Image files not inside the image zip file. These files should be downloaded
 # directly from devserver.
@@ -116,6 +123,12 @@
 
     _parser = autoserv_parser.autoserv_parser
 
+    # Minimum build id that supports server side packaging. Older builds may
+    # not have server side package built or with Autotest code change to support
+    # server-side packaging.
+    MIN_VERSION_SUPPORT_SSP = CONFIG.get_config_value(
+            'AUTOSERV', 'min_launch_control_build_id_support_ssp', type=int)
+
     @staticmethod
     def check_host(host, timeout=10):
         """
@@ -590,7 +603,10 @@
             serial = self.adb_serial
             # ADB has a device state, if the device is not online, no
             # subsequent ADB command will complete.
-            if serial not in devices or not self.is_device_ready():
+            # DUT with single device connected may not have adb_serial set.
+            # Therefore, skip checking if serial is in the list of adb devices
+            # if self.adb_serial is not set.
+            if (serial and serial not in devices) or not self.is_device_ready():
                 logging.debug('Waiting for device to enter the ready state.')
                 return False
         elif command == FASTBOOT_CMD:
@@ -988,8 +1004,8 @@
         @param build_url: The url to use for downloading Android artifacts.
                 pattern: http://$devserver:###/static/branch/target/build_id
 
-        @return: A dictionary of build information, including keys: board,
-                 branch, target, build_id.
+        @return: A dictionary of build information, including keys:
+                 build_target, branch, target, build_id.
         @raise AndroidInstallError: If failed to parse build_url.
         """
         if not build_url:
@@ -997,9 +1013,9 @@
 
         try:
             match = re.match(DEVSERVER_URL_REGEX, build_url)
-            return {'board': match.group('BOARD'),
+            return {'build_target': match.group('BUILD_TARGET'),
                     'branch': match.group('BRANCH'),
-                    'target': ('%s-%s' % (match.group('BOARD'),
+                    'target': ('%s-%s' % (match.group('BUILD_TARGET'),
                                           match.group('BUILD_TYPE'))),
                     'build_id': match.group('BUILD_ID')}
         except (AttributeError, IndexError, ValueError) as e:
@@ -1104,7 +1120,7 @@
         devserver = dev_server.AndroidBuildServer.resolve(build_name,
                                                           self.hostname)
         build_name = devserver.translate(build_name)
-        branch, target, build_id = utils.parse_android_build(build_name)
+        branch, target, build_id = utils.parse_launch_control_build(build_name)
         is_brillo = os_type == OS_TYPE_BRILLO
         devserver.trigger_download(target, build_id, branch,
                                    is_brillo=is_brillo, synchronous=False)
@@ -1362,3 +1378,57 @@
     def update_labels(self):
         """Update the labels for this testbed."""
         self.labels.update_labels(self)
+
+
+    def stage_server_side_package(self, image=None):
+        """Stage autotest server-side package on devserver.
+
+        @param image: A build name, e.g., git_mnc_dev/shamu-eng/123
+
+        @return: A url to the autotest server-side package. Return None if
+                 server-side package is not supported.
+        @raise: error.AutoservError if fail to locate the build to test with.
+        """
+        if image:
+            ds = dev_server.AndroidBuildServer.resolve(image, self.hostname)
+        else:
+            job_repo_url = afe_utils.get_host_attribute(
+                    self, self.job_repo_url_attribute)
+            if job_repo_url:
+                devserver_url, image = (
+                        tools.get_devserver_build_from_package_url(
+                                job_repo_url, True))
+                ds = dev_server.AndroidBuildServer(devserver_url)
+            else:
+                labels = afe_utils.get_labels(self, self.VERSION_PREFIX)
+                if not labels:
+                    raise error.AutoservError(
+                            'Failed to stage server-side package. The host has '
+                            'no job_report_url attribute or version label.')
+                image = labels[0].name[len(self.VERSION_PREFIX)+1:]
+                ds = dev_server.AndroidBuildServer.resolve(image, self.hostname)
+
+        branch, target, build_id = utils.parse_launch_control_build(image)
+        build_target, _ = utils.parse_launch_control_target(target)
+
+        # For any build older than MIN_VERSION_SUPPORT_SSP, server side
+        # packaging is not supported.
+        try:
+            if int(build_id) < self.MIN_VERSION_SUPPORT_SSP:
+                logging.warn('Build %s is older than %s. Server side packaging '
+                             'is disabled.', image,
+                             self.MIN_VERSION_SUPPORT_SSP)
+                return None
+        except ValueError:
+            logging.warn('Failed to compare build id in %s with the minimum '
+                         'version that supports server side packaging. Server '
+                         'side packaging is disabled.', image)
+            return None
+
+        ds.stage_artifacts(target, build_id, branch,
+                           artifacts=['autotest_server_package'])
+        autotest_server_package_name = (AUTOTEST_SERVER_PACKAGE_FILE_FMT %
+                                        {'build_target': build_target,
+                                         'build_id': build_id})
+        return '%s/static/%s/%s' % (ds.url(), image,
+                                    autotest_server_package_name)
diff --git a/server/hosts/factory.py b/server/hosts/factory.py
index b820933..be85453 100644
--- a/server/hosts/factory.py
+++ b/server/hosts/factory.py
@@ -4,6 +4,7 @@
 from contextlib import closing
 
 from autotest_lib.client.bin import local_host
+from autotest_lib.client.bin import utils
 from autotest_lib.client.common_lib import error, global_config
 from autotest_lib.server import utils as server_utils
 from autotest_lib.server.hosts import cros_host, ssh_host
@@ -11,9 +12,9 @@
 from autotest_lib.server.hosts import adb_host, testbed
 
 
-SSH_ENGINE = global_config.global_config.get_config_value('AUTOSERV',
-                                                          'ssh_engine',
-                                                          type=str)
+CONFIG = global_config.global_config
+
+SSH_ENGINE = CONFIG.get_config_value('AUTOSERV', 'ssh_engine', type=str)
 
 # Default ssh options used in creating a host.
 DEFAULT_SSH_USER = 'root'
@@ -198,6 +199,24 @@
 
     @returns: The target machine to be used for verify/repair.
     """
+    # For Brillo/Android devices connected to moblab, the `machine` name is
+    # either `localhost` or `127.0.0.1`. It needs to be translated to the host
+    # container IP if the code is running inside a container. This way, autoserv
+    # can ssh to the moblab and run actual adb/fastboot commands.
+    is_moblab = CONFIG.get_config_value('SSP', 'is_moblab', type=bool,
+                                        default=False)
+    hostname = machine['hostname'] if isinstance(machine, dict) else machine
+    if (utils.is_in_container() and is_moblab and
+        hostname in ['localhost', '127.0.0.1']):
+        hostname = CONFIG.get_config_value('SSP', 'host_container_ip', type=str,
+                                           default=None)
+        if isinstance(machine, dict):
+            machine['hostname'] = hostname
+        else:
+            machine = hostname
+        logging.debug('Hostname of machine is converted to %s for the test to '
+                      'run inside a container.', hostname)
+
     # TODO(kevcheng): We'll want to have a smarter way of figuring out which
     # host to create (checking host labels).
     if server_utils.machine_is_testbed(machine):
diff --git a/site_utils/suite_scheduler/driver.py b/site_utils/suite_scheduler/driver.py
index e63697b..d3bebdc 100644
--- a/site_utils/suite_scheduler/driver.py
+++ b/site_utils/suite_scheduler/driver.py
@@ -213,7 +213,7 @@
         else:
             logging.info('Build is not a ChromeOS build, try to parse as a '
                          'Launch Control build.')
-            _,target,_ = utils.parse_android_build(build_name)
+            _,target,_ = utils.parse_launch_control_build(build_name)
             board = '%s-%s' % (os_type, utils.parse_launch_control_target(target)[0])
             launch_control_builds = [build_name]
             logging.info('Testing Launch Control build %s on %s', build_name,