Initial xBuddy for devserver

Contains most of the basic functionality of the xBuddy rpc, as outlined
in the Design Doc found in ChromeOs Installer.
  - xbuddy, xbuddy_list, xbuddy_capacity rpcs on devserver
  - xbuddy's path translation:
    - defined default version aliases
    - defined default xbuddy artifact aliases
  - xbuddy build cache:
    - on build_id cache hit, serves corresponding image/artifact
    - on build_id cache miss, downloads from Google Storage, then serves
    - maintains a cache of 5 downloaded builds & record of their last
      access time in a separate timestamp directory

Plus some housekeeping of devserver constants

BUG=chromium:252941
TEST=manual and unit tests

Manual (for testing devserver rpcs): Run the devserver locally, attempt
to access each of the following addresses from browser

  1. http://localhost:8080/xbuddy?path=/parrot-release/R21-2461.0.0/
  test&return_update_url=t
  Expect: Several seconds of lag as image is downloaded, then a
  url to the image dir, such as
  http://localhost:8080/static/parrot-release/R21-2461.0.0
  [Note, using an IP address instead of localhost should return that IP
  address]

  2. http://localhost:8080/xbuddy?path=/parrot-release/R21-2461.0.0/test
  Expect: A download of the right chromeos_test_image.bin

  3. http://localhost:8080/xbuddy_capacity/
  Expect: Just '5', the default xbuddy capacity

  4. http://localhost:8080/xbuddy_list/
  Expect: A string that lists the previously requested build and how
  long ago it was accessed

  5. More combinations of board/version/alias should work as well, with
  xbuddy_list and the default devserver static folder's contents
  reflecting normal caching behavior.

Unit Tests (for xbuddy functions): run xbuddy_unittests.py

Change-Id: I612cbb3ee907bb70907669d6db20f266157c0244
Reviewed-on: https://gerrit.chromium.org/gerrit/59287
Reviewed-by: Joy Chen <joychen@chromium.org>
Tested-by: Joy Chen <joychen@chromium.org>
Commit-Queue: Joy Chen <joychen@chromium.org>
diff --git a/devserver.py b/devserver.py
index 661ed6f..b80744b 100755
--- a/devserver.py
+++ b/devserver.py
@@ -61,6 +61,7 @@
 import common_util
 import downloader
 import log_util
+import xbuddy
 
 # Module-local log function.
 def _Log(message, *args):
@@ -381,9 +382,10 @@
   # Lock used to lock increasing/decreasing count.
   _staging_thread_count_lock = threading.Lock()
 
-  def __init__(self):
+  def __init__(self, _xbuddy):
     self._builder = None
     self._telemetry_lock_dict = common_util.LockDict()
+    self._xbuddy = _xbuddy
 
   @cherrypy.expose
   def build(self, board, pkg, **kwargs):
@@ -695,6 +697,64 @@
         image_types_list))
 
   @cherrypy.expose
+  def xbuddy(self, **kwargs):
+    """The full xBuddy call, returns path to resource on this devserver.
+
+    Args:
+      path: build_id/alias
+        build_id is composed of "board/version"
+        The board is the familiar board name, optionally suffixed.
+        The version can be the google storage version number, and may also be
+        one of a number of aliases that will be translated into the latest
+        built image that fits the description.
+        The alias is one of a number of image or artifact aliases used by
+        xbuddy, defined in xbuddy:ALIASES
+      return_dir: {true|false}
+                  if set to true, returns the url to the update.gz
+                  instead.
+
+    Example URL:
+      http://host:port/xbuddy?path=/x86-generic/R26-4000.0.0/test
+      or
+      http://host:port/xbuddy?path=/x86-generic/R26-4000.0.0/
+      test&return_dir=true
+
+    Returns:
+      A redirect to the image or update file on the devserver.
+      e.g. http://host:port/static/archive/x86-generic-release/
+      R26-4000.0.0/chromium-test-image.bin
+      or if return_dir is True, return path to the folder where
+      image or update file is
+      http://host:port/static/x86-generic-release/R26-4000.0.0/
+    """
+    boolean_string = kwargs.get('return_dir')
+    return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
+    devserver_url = cherrypy.request.base
+    return_url = self._xbuddy.Get(kwargs.get('path'),
+                                  return_dir)
+    if return_dir:
+      return os.path.join(devserver_url, return_url)
+    else:
+      raise cherrypy.HTTPRedirect(return_url, 302)
+
+  @cherrypy.expose
+  def xbuddy_list(self):
+    """Lists the currently available images & time since last access.
+
+    @return: A string representation of a list of tuples
+      [(build_id, time since last access),...]
+    """
+    return self._xbuddy.List()
+
+  @cherrypy.expose
+  def xbuddy_capacity(self):
+    """Returns the number of images cached by xBuddy.
+
+    @return: Capacity of this devserver.
+    """
+    return self._xbuddy.Capacity()
+
+  @cherrypy.expose
   def index(self):
     """Presents a welcome message and documentation links."""
     return ('Welcome to the Dev Server!<br>\n'
@@ -1015,7 +1075,10 @@
   if options.exit:
     return
 
-  cherrypy.quickstart(DevServerRoot(), config=_GetConfig(options))
+  _xbuddy = xbuddy.XBuddy(static_dir)
+  dev_server = DevServerRoot(_xbuddy)
+
+  cherrypy.quickstart(dev_server, config=_GetConfig(options))
 
 
 if __name__ == '__main__':