[autotest] Be able to specify LATEST build as target build.

This change allows the user to specify running a suite against
a build name in the format [builder]/LATEST. This allows for users
to just use the latest build without looking it up.

BUG=chromium:375354
TEST=Tested running from run_suite, and afe. Then created a recurring
job that worked as desired.
DEPLOY=apache
CQ-DEPEND=Iddc2ab2b0675591b53311ebe9e1692d0c0859377

Change-Id: I5c0cd9ad06bd016b539902248e193115704ae243
Reviewed-on: https://chromium-review.googlesource.com/200822
Reviewed-by: Simran Basi <sbasi@chromium.org>
Tested-by: Simran Basi <sbasi@chromium.org>
Commit-Queue: Simran Basi <sbasi@chromium.org>
diff --git a/client/common_lib/cros/dev_server.py b/client/common_lib/cros/dev_server.py
index 9d4aa16..774bf28 100644
--- a/client/common_lib/cros/dev_server.py
+++ b/client/common_lib/cros/dev_server.py
@@ -3,19 +3,20 @@
 # found in the LICENSE file.
 
 from distutils import version
+import cStringIO
+import HTMLParser
 import httplib
 import json
 import logging
-import urllib2
-import HTMLParser
-import cStringIO
+import os
 import re
 import sys
+import urllib2
 
+from autotest_lib.client.bin import utils as site_utils
 from autotest_lib.client.common_lib import global_config
 from autotest_lib.client.common_lib import utils
 from autotest_lib.client.common_lib.cros import retry
-from autotest_lib.client.bin import utils as site_utils
 from autotest_lib.site_utils.graphite import stats
 # TODO(cmasone): redo this class using requests module; http://crosbug.com/30107
 
@@ -478,6 +479,7 @@
         @raise DevServerException upon any return code that's not HTTP OK.
         """
         assert artifacts or files, 'Must specify something to stage.'
+        image = self.translate(image)
         if not archive_url:
             archive_url = (_get_storage_server_for_artifacts(artifacts) +
                            image)
@@ -503,6 +505,7 @@
 
         @raise DevServerException upon any return code that's not HTTP OK.
         """
+        image = self.translate(image)
         logging.info('Requesting contents from devserver %s for image %s',
                      self.url(), image)
         archive_url = _get_storage_server_for_artifacts() + image
@@ -532,6 +535,7 @@
         @raise DevServerException upon any return code that's not HTTP OK.
 
         """
+        image = self.translate(image)
         archive_url = _get_image_storage_server() + image
         artifacts = _ARTIFACTS_TO_BE_STAGED_FOR_IMAGE
         error_message = ("trigger_download for %s failed;"
@@ -557,6 +561,7 @@
 
         @returns path on the devserver that telemetry is installed to.
         """
+        build = self.translate(build)
         archive_url = _get_image_storage_server() + build
         call = self.build_call('setup_telemetry', archive_url=archive_url)
         try:
@@ -581,6 +586,7 @@
         @param image: the image to fetch and stage.
         @raise DevServerException upon any return code that's not HTTP OK.
         """
+        image = self.translate(image)
         archive_url = _get_image_storage_server() + image
         artifacts = _ARTIFACTS_TO_BE_STAGED_FOR_IMAGE_WITH_AUTOTEST
         error_message = ("finish_download for %s failed;"
@@ -597,6 +603,7 @@
 
         @param image: the image that was fetched.
         """
+        image = self.translate(image)
         url_pattern = CONFIG.get_config_value('CROS', 'image_url_pattern',
                                               type=str)
         return (url_pattern % (self.url(), image))
@@ -607,6 +614,7 @@
 
         @param image: the image that was fetched.
         """
+        image = self.translate(image)
         url_pattern = CONFIG.get_config_value('CROS', 'image_url_pattern',
                                               type=str)
         return (url_pattern % (self.url(), image)).replace(
@@ -654,6 +662,7 @@
                 (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)
         response = urllib2.urlopen(call)
@@ -671,6 +680,7 @@
         @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 urllib2.urlopen(call).read()
@@ -690,6 +700,7 @@
                 a dict of dicts, as per site_utils/suite_preprocessor.py.
         @raise DevServerException upon any return code that's not HTTP OK.
         """
+        build = self.translate(build)
         call = self.build_call('controlfiles',
                                build=build, control_path=DEPENDENCIES_FILE)
         return urllib2.urlopen(call).read()
@@ -719,6 +730,40 @@
             return None
 
 
+    @remote_devserver_call()
+    def get_latest_build_in_gs(self, board):
+        """Ask the devservers for the latest offical build in Google Storage.
+
+        @param board: The board for who we want the latest official build.
+        @return A string of the returned build rambi-release/R37-5868.0.0
+        @raise DevServerException upon any return code that's not HTTP OK.
+        """
+        call = self.build_call(
+                'xbuddy_translate/remote/%s/latest-official' % board,
+                image_dir=_get_image_storage_server())
+        image_name = urllib2.urlopen(call).read()
+        return os.path.dirname(image_name)
+
+
+    def translate(self, build_name):
+        """Translate the build name if it's in LATEST format.
+
+        If the build name is in the format [builder]/LATEST, return the latest
+        build in Google Storage otherwise return the build name as is.
+
+        @param build_name: build_name to check.
+
+        @return The actual build name to use.
+        """
+        match = re.match(r'([\w-]+)-(\w+)/LATEST', build_name)
+        if not match:
+            return build_name
+        translated_build = self.get_latest_build_in_gs(match.groups()[0])
+        logging.debug('Translated relative build %s to %s', build_name,
+                      translated_build)
+        return translated_build
+
+
     @classmethod
     @remote_devserver_call()
     def get_latest_build(cls, target, milestone=''):
diff --git a/server/cros/dynamic_suite/dynamic_suite.py b/server/cros/dynamic_suite/dynamic_suite.py
index ef6b823..5380740 100644
--- a/server/cros/dynamic_suite/dynamic_suite.py
+++ b/server/cros/dynamic_suite/dynamic_suite.py
@@ -310,9 +310,9 @@
             if not value or not isinstance(value, expected):
                 raise error.SuiteArgumentException(
                     "reimage_and_run() needs %s=<%r>" % (key, expected))
-        self.build = build
         self.board = 'board:%s' % board
         self.devserver = dev_server.ImageServer(devserver_url)
+        self.build = self.devserver.translate(build)
         self.name = name
         self.job = job
         if pool:
diff --git a/server/site_utils.py b/server/site_utils.py
index 6f4c905..528a9a9 100644
--- a/server/site_utils.py
+++ b/server/site_utils.py
@@ -42,17 +42,22 @@
 def ParseBuildName(name):
     """Format a build name, given board, type, milestone, and manifest num.
 
-    @param name: a build name, e.g. 'x86-alex-release/R20-2015.0.0'
+    @param name: a build name, e.g. 'x86-alex-release/R20-2015.0.0' or a
+                 relative build name, e.g. 'x86-alex-release/LATEST'
 
     @return board: board the manifest is for, e.g. x86-alex.
     @return type: one of 'release', 'factory', or 'firmware'
     @return milestone: (numeric) milestone the manifest was associated with.
-    @return manifest: manifest number, e.g. '2015.0.0'
+                        Will be None for relative build names.
+    @return manifest: manifest number, e.g. '2015.0.0'.
+                      Will be None for relative build names.
 
     """
-    match = re.match(r'([\w-]+)-(\w+)/R(\d+)-([\d.ab-]+)', name)
-    if match and len(match.groups()) == 4:
-        return match.groups()
+    match = re.match(r'(?P<board>[\w-]+)-(?P<type>\w+)/(R(?P<milestone>\d+)-'
+                     r'(?P<manifest>[\d.ab-]+)|LATEST)', name)
+    if match and len(match.groups()) == 5:
+        return (match.group('board'), match.group('type'),
+                match.group('milestone'), match.group('manifest'))
     raise ParseBuildNameException('%s is a malformed build name.' % name)