Devserver update rpc: translate xBuddy paths.

If an update request starts with 'xbuddy' the rest of the path will be
translated to a directory within the devserver's static_dir before being
passed onto the normal update call.

BUG=chromium:261671
TEST=Effectively None, as there aren't any scripts that use the update
rpc with xBuddy yet. :(

Can call http://host:port/update/xbuddy/board/version/ to see that the
translation happens, but without the xml, this isn't meaningful yet.

Change-Id: I072ba04045a30b3c7d03d67556f9f9d0c7af41ec
Reviewed-on: https://gerrit.chromium.org/gerrit/63282
Reviewed-by: Chris Sosa <sosa@chromium.org>
Commit-Queue: Joy Chen <joychen@chromium.org>
Reviewed-by: Joy Chen <joychen@chromium.org>
Tested-by: Joy Chen <joychen@chromium.org>
diff --git a/devserver.py b/devserver.py
index faec8a1..5b82608 100755
--- a/devserver.py
+++ b/devserver.py
@@ -792,7 +792,15 @@
     Example:
       http://myhost/update/optional/path/to/payload
     """
-    label = '/'.join(args)
+    if len(args) > 0 and args[0] == 'xbuddy':
+      # Interpret the rest of the path as an xbuddy path
+      label, found = self._xbuddy.Translate(args[1:] + ('full_payload',))
+      if not found:
+        _Log("Update payload not found for %s, xBuddy looking it up.", label)
+    else:
+      label = '/'.join(args)
+
+    _Log('Update label: %s', label)
     body_length = int(cherrypy.request.headers.get('Content-Length', 0))
     data = cherrypy.request.rfile.read(body_length)
     return updater.HandleUpdatePing(data, label)
diff --git a/xbuddy.py b/xbuddy.py
index d04d96e..1bd4b6d 100644
--- a/xbuddy.py
+++ b/xbuddy.py
@@ -33,13 +33,14 @@
   'test',
   'base',
   'dev',
+  'full_payload',
 ]
 
 LOCAL_FILE_NAMES = [
   devserver_constants.TEST_IMAGE_FILE,
-
   devserver_constants.BASE_IMAGE_FILE,
   devserver_constants.IMAGE_FILE,
+  devserver_constants.ROOT_UPDATE_FILE,
 ]
 
 LOCAL_ALIAS_TO_FILENAME = dict(zip(LOCAL_ALIASES, LOCAL_FILE_NAMES))
@@ -277,6 +278,7 @@
 
     Returns:
       version - the discovered version of the image.
+      found - True if file was found
     """
     latest_local_dir = self.GetLatestImageDir(board)
     if not latest_local_dir and os.path.exists(latest_local_dir):
@@ -287,10 +289,10 @@
     version = os.path.basename(latest_local_dir)
 
     path_to_image = os.path.join(latest_local_dir, file_name)
-    if not os.path.exists(path_to_image):
-      raise XBuddyException('%s not found in %s. Did you run build_image?' %
-                            (file_name, latest_local_dir))
-    return version
+    if os.path.exists(path_to_image):
+      return version, True
+    else:
+      return version, False
 
   def _InterpretPath(self, path_list):
     """
@@ -343,7 +345,7 @@
       # This path doesn't have an alias or a version. That's fine.
       _Log("Some parts of the path not specified. Using defaults.")
 
-    _Log("Get artifact '%s' in {%s/%s}. Locally? %s",
+    _Log("Get artifact '%s' in '%s/%s'. Locally? %s",
          image_type, board, version, is_local)
 
     return image_type, board, version, is_local
@@ -437,26 +439,45 @@
       except Exception:
         raise XBuddyException('Failed to clear build in %s.' % clear_dir)
 
-  def _GetFromGS(self, build_id, image_type):
-    """Check if the artifact is available locally. Download from GS if not."""
+  def _GetFromGS(self, build_id, image_type, lookup_only):
+    """Check if the artifact is available locally. Download from GS if not.
+
+    Return:
+      boolean - True if cached.
+    """
     gs_url = os.path.join(devserver_constants.GS_IMAGE_DIR,
                           build_id)
 
     # stage image if not found in cache
     file_name = GS_ALIAS_TO_FILENAME[image_type]
-    cached = os.path.exists(os.path.join(self.static_dir,
-                                         build_id,
-                                         file_name))
+    file_loc = os.path.join(self.static_dir, build_id, file_name)
+    cached = os.path.exists(file_loc)
+
     if not cached:
-      artifact = GS_ALIAS_TO_ARTIFACT[image_type]
-      self._Download(gs_url, artifact)
+      if lookup_only:
+        return False
+      else:
+        artifact = GS_ALIAS_TO_ARTIFACT[image_type]
+        self._Download(gs_url, artifact)
+        return True
     else:
       _Log('Image already cached.')
+      return True
 
-  def _GetArtifact(self, path):
-    """Interpret an xBuddy path and return directory/file_name to resource."""
+  def _GetArtifact(self, path, lookup_only=False):
+    """Interpret an xBuddy path and return directory/file_name to resource.
+
+    Returns:
+    image_url to the directory
+    file_name of the artifact
+    found = True if the artifact is cached
+
+    Raises:
+    XBuddyException if the path could not be translated
+    """
     image_type, board, version, is_local = self._InterpretPath(path)
 
+    found = False
     if is_local:
       # Get a local image
       if image_type not in LOCAL_ALIASES:
@@ -466,12 +487,12 @@
 
       if version == LATEST:
         # Get the latest local image for the given board
-        version = self._GetLatestLocalVersion(board, file_name)
+        version, found = self._GetLatestLocalVersion(board, file_name)
       else:
         # An exact version path in build/images was specified for this board
         local_file = os.path.join(self.images_dir, board, version, file_name)
-        if not os.path.exists(local_file):
-          raise XBuddyException('File not found in local dir: %s', local_file)
+        if os.path.exists(local_file):
+          found = True
 
       image_url = os.path.join(board, version)
     else:
@@ -483,9 +504,9 @@
 
       # Interpret the version (alias), and get gs address
       image_url = self._ResolveVersionToUrl(board, version)
-      self._GetFromGS(image_url, image_type)
+      found = self._GetFromGS(image_url, image_type, lookup_only)
 
-    return image_url, file_name
+    return image_url, file_name, found
 
   ############################ BEGIN PUBLIC METHODS
 
@@ -503,6 +524,22 @@
     """Returns the number of images cached by xBuddy."""
     return str(_XBUDDY_CAPACITY)
 
+  def Translate(self, path_list):
+    """Translates an xBuddy path to a real path to artifact if it exists.
+
+    Equivalent to the Get call, minus downloading and updating timestamps.
+    The returned path is always the path to the directory.
+
+    Throws:
+      XBuddyException - if the path couldn't be translated
+    """
+    self._SyncRegistryWithBuildImages()
+
+    build_id, _file_name, found = self._GetArtifact(path_list, lookup_only=True)
+
+    _Log('Returning path to payload: %s', build_id)
+    return build_id, found
+
   def Get(self, path_list, return_dir=False):
     """The full xBuddy call, returns resource specified by path_list.
 
@@ -519,10 +556,10 @@
       http://host/static/x86-generic/R26-4000.0.0/
 
     Raises:
-      XBuddyException if path is invalid or XBuddy's cache fails
+      XBuddyException if path is invalid
     """
     self._SyncRegistryWithBuildImages()
-    build_id, file_name = self._GetArtifact(path_list)
+    build_id, file_name, _found = self._GetArtifact(path_list)
 
     Timestamp.UpdateTimestamp(self._timestamp_folder, build_id)