XBuddy: Map base versions into fully qualified build version.

This uses LATEST-x.y.z markers in GS to map base version numbers (e.g.
6801.0.0) to fully qualified build versions including a release number
(e.g. R42-6801.0.0) for a given board. This is needed in order to fetch
SDK images corresponding to an SDK version, which does not include
a release number.

BUG=brillo:336
TEST=Unit tests.
TEST=Manual test with cros flash --project-sdk.

Change-Id: I59dd6294d52bbb4247e8c3c5066a5a2ff753508e
Reviewed-on: https://chromium-review.googlesource.com/251468
Trybot-Ready: Gilad Arnold <garnold@chromium.org>
Tested-by: Gilad Arnold <garnold@chromium.org>
Reviewed-by: Chris Sosa <sosa@chromium.org>
Commit-Queue: Gilad Arnold <garnold@chromium.org>
Reviewed-by: Don Garrett <dgarrett@chromium.org>
diff --git a/devserver_constants.py b/devserver_constants.py
index 222a1e5..35ac8b9 100644
--- a/devserver_constants.py
+++ b/devserver_constants.py
@@ -9,6 +9,8 @@
 CHANNELS = 'canary', 'dev', 'beta', 'stable'
 GS_IMAGE_DIR = 'gs://chromeos-image-archive'
 GS_LATEST_MASTER = '%(image_dir)s/%(board)s%(suffix)s/LATEST-master'
+GS_LATEST_BASE_VERSION = (
+    '%(image_dir)s/%(board)s%(suffix)s/LATEST-%(base_version)s')
 IMAGE_DIR = '%(board)s%(suffix)s/%(version)s'
 
 GS_RELEASES_DIR = 'gs://chromeos-releases'
diff --git a/xbuddy.py b/xbuddy.py
index 0bab714..5e7dfcb 100644
--- a/xbuddy.py
+++ b/xbuddy.py
@@ -329,6 +329,7 @@
     return devserver_constants.IMAGE_DIR % {'board':board,
                                             'suffix':suffix,
                                             'version':version}
+
   def _LookupChannel(self, board, channel='stable', image_dir=None):
     """Check the channel folder for the version number of interest."""
     # Get all names in channel dir. Get 10 highest directories by version.
@@ -398,6 +399,21 @@
     raise XBuddyException('Could not find remote build_id for %s %s' % (
         board, version))
 
+  def _ResolveBuildVersion(self, board, base_version):
+    """Check LATEST-<base_version> and returns a full build version."""
+    _Log('Checking gs for full version for %s of %s', base_version, board)
+    # TODO(garnold) We might want to accommodate version prefixes and pick the
+    # most recent found, as done in _LookupVersion().
+    latest_addr = (devserver_constants.GS_LATEST_BASE_VERSION %
+                   {'image_dir': devserver_constants.GS_IMAGE_DIR,
+                    'board': board,
+                    'suffix': RELEASE,
+                    'base_version': base_version})
+    cmd = 'gsutil cat %s' % latest_addr
+    msg = 'Failed to find build at %s' % latest_addr
+    # Full release + version is in the LATEST file.
+    return gsutil_util.GSUtilRun(cmd, msg)
+
   def _ResolveVersionToBuildId(self, board, version, image_dir=None):
     """Handle version aliases for remote payloads in GS.
 
@@ -405,10 +421,11 @@
       board: as specified in the original call. (i.e. x86-generic, parrot)
       version: as entered in the original call. can be
         {TBD, 0. some custom alias as defined in a config file}
-        1. latest
-        2. latest-{channel}
-        3. latest-official-{board suffix}
-        4. version prefix (i.e. RX-Y.X, RX-Y, RX)
+        1. fully qualified build version or base version.
+        2. latest
+        3. latest-{channel}
+        4. latest-official-{board suffix}
+        5. version prefix (i.e. RX-Y.X, RX-Y, RX)
       image_dir: image directory to check in Google Storage. If none,
         the default bucket is used.
 
@@ -423,6 +440,9 @@
 
     if re.match(devserver_constants.VERSION_RE, version):
       return self._RemoteBuildId(board, version)
+    elif re.match(devserver_constants.VERSION, version):
+      return self._RemoteBuildId(board,
+                                 self._ResolveBuildVersion(board, version))
     elif version == LATEST_OFFICIAL:
       # latest-official --> LATEST build in board-release
       return self._LookupOfficial(board, image_dir=image_dir)
diff --git a/xbuddy_unittest.py b/xbuddy_unittest.py
index 074ebac..e31e18e 100755
--- a/xbuddy_unittest.py
+++ b/xbuddy_unittest.py
@@ -132,6 +132,19 @@
                                           image_dir=GS_ALTERNATE_DIR)
     self.mox.VerifyAll()
 
+  def testResolveVersionToBuildId_BaseVersion(self):
+    """Check _ResolveVersionToBuildId handles a base version."""
+    board = 'b'
+
+    self.mox.StubOutWithMock(self.mock_xb, '_ResolveBuildVersion')
+    self.mock_xb._ResolveBuildVersion(board, '1.2.3').AndReturn('R12-1.2.3')
+    self.mox.StubOutWithMock(self.mock_xb, '_RemoteBuildId')
+    self.mock_xb._RemoteBuildId(board, 'R12-1.2.3')
+    self.mox.ReplayAll()
+
+    self.mock_xb._ResolveVersionToBuildId(board, '1.2.3')
+    self.mox.VerifyAll()
+
   def testBasicInterpretPath(self):
     """Basic checks for splitting a path"""
     path = 'parrot/R27-2455.0.0/test'