adds Devserver support for statically serving versioned updates.

Also decouples it from the chroot when run in serve-only mode.

Review URL: http://codereview.chromium.org/1141001
diff --git a/autoupdate.py b/autoupdate.py
index eb5b9ac..4461533 100644
--- a/autoupdate.py
+++ b/autoupdate.py
@@ -9,21 +9,27 @@
 import web
 
 class Autoupdate(BuildObject):
-
   # Basic functionality of handling ChromeOS autoupdate pings 
   # and building/serving update images.
   # TODO(rtc): Clean this code up and write some tests.
 
+  def __init__(self, serve_only=None, test_image=False, *args, **kwargs):
+    self.serve_only = serve_only
+    if serve_only:
+      web.debug('Autoupdate in "serve update images only" mode.')
+    self.test_image=test_image
+    super(Autoupdate, self).__init__(*args, **kwargs)
+
   def GetUpdatePayload(self, hash, size, url):
     payload = """<?xml version="1.0" encoding="UTF-8"?>
       <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
         <app appid="{%s}" status="ok">
           <ping status="ok"/>
-          <updatecheck 
-            codebase="%s" 
-            hash="%s" 
-            needsadmin="false" 
-            size="%s" 
+          <updatecheck
+            codebase="%s"
+            hash="%s"
+            needsadmin="false"
+            size="%s"
             status="ok"/>
         </app>
       </gupdate>
@@ -42,14 +48,14 @@
     return payload % self.app_id
 
   def GetLatestImagePath(self, board_id):
-    cmd = "%s/get_latest_image.sh --board %s" % (self.scripts_dir, board_id)
+    cmd = '%s/get_latest_image.sh --board %s' % (self.scripts_dir, board_id)
     return os.popen(cmd).read().strip()
 
   def GetLatestVersion(self, latest_image_path):
     latest_version = latest_image_path.split('/')[-1]
 
     # Removes the portage build prefix.
-    latest_version = latest_version.lstrip("g-")
+    latest_version = latest_version.lstrip('g-')
     return latest_version.split('-')[0]
 
   def CanUpdate(self, client_version, latest_version):
@@ -58,7 +64,7 @@
     """
     client_tokens = client_version.split('.')
     latest_tokens = latest_version.split('.')
-    web.debug("client version %s latest version %s" \
+    web.debug('client version %s latest version %s' \
         % (client_version, latest_version))
     for i in range(0,4):
       if int(latest_tokens[i]) == int(client_tokens[i]):
@@ -67,56 +73,88 @@
     return False
 
   def BuildUpdateImage(self, image_path):
-    image_file = "%s/rootfs.image" % image_path
-    web.debug("checking image file %s/update.gz" % image_path)
-    if not os.path.exists("%s/update.gz" % image_path):
-      mkupdate = "%s/mk_memento_images.sh %s" % (self.scripts_dir, image_file)
+    if self.test_image:
+      image_file = '%s/rootfs_test.image' % image_path
+    else:
+      image_file = '%s/rootfs.image' % image_path
+    update_file = '%s/update.gz' % image_path
+    if (os.path.exists(update_file) and
+        os.path.getmtime(update_file) >= os.path.getmtime(image_file)):
+      web.debug('Found cached update image %s/update.gz' % image_path)
+    else:
+      web.debug('generating update image %s/update.gz' % image_path)
+      mkupdate = '%s/mk_memento_images.sh %s' % (self.scripts_dir, image_file)
       web.debug(mkupdate)
       err = os.system(mkupdate)
       if err != 0:
-        web.debug("failed to create update image")
+        web.debug('failed to create update image')
         return False
-
-    web.debug("Found an image, copying it to static")
-    err = os.system("cp %s/update.gz %s" % (image_path, self.static_dir))
-    if err != 0:
-      web.debug("Unable to move update.gz from %s to %s" \
-          % (image_path, self.static_dir))
-      return False
+    if not self.serve_only:
+      web.debug('Found an image, copying it to static')
+      try:
+        shutil.copyfile('%s/update.gz' % image_path, self.static_dir)
+      except Exception, e:
+        web.debug('Unable to copy update.gz from %s to %s' \
+                  % (image_path, self.static_dir))
+        return False
     return True
 
   def GetSize(self, update_path):
     return os.path.getsize(update_path)
 
   def GetHash(self, update_path):
-    cmd = "cat %s | openssl sha1 -binary | openssl base64 | tr \'\\n\' \' \';" \
-        % update_path
-    web.debug(cmd)
-    return os.popen(cmd).read()
+    update_dir = os.path.dirname(update_path)
+    if not os.path.exists('%s/cksum' % update_dir):
+      cmd = ('cat %s | openssl sha1 -binary'
+             '| openssl base64 | tr \'\\n\' \' \' | tee %s/cksum' %
+             (update_path, update_dir))
+      web.debug(cmd)
+      return os.popen(cmd).read()
+    else:
+      web.debug('using cached checksum for %s' % update_path)
+      return file('%s/cksum' % update_dir).read()
 
-  def HandleUpdatePing(self, data):
+  def HandleUpdatePing(self, data, label=None):
     update_dom = minidom.parseString(data)
     root = update_dom.firstChild
-    query = root.getElementsByTagName("o:app")[0]
+    query = root.getElementsByTagName('o:app')[0]
     client_version = query.getAttribute('version')
     board_id = query.hasAttribute('board') and query.getAttribute('board') \
-        or "x86-generic"
+        or 'x86-generic'
     latest_image_path = self.GetLatestImagePath(board_id)
     latest_version = self.GetLatestVersion(latest_image_path)
-    if client_version != "ForcedUpdate" \
+    if client_version != 'ForcedUpdate' \
         and not self.CanUpdate(client_version, latest_version):
-      web.debug("no update")
+      web.debug('no update')
       return self.GetNoUpdatePayload()
-
-    web.debug("update found %s " % latest_version)
-    ok = self.BuildUpdateImage(latest_image_path)
-    if ok != True:
-      web.debug("Failed to build an update image")
-      return self.GetNoUpdatePayload()
-
-    hash = self.GetHash("%s/update.gz" % self.static_dir)
-    size = self.GetSize("%s/update.gz" % self.static_dir)
     hostname = web.ctx.host
-    url = "http://%s/static/update.gz" % hostname
-    return self.GetUpdatePayload(hash, size, url)
+    if label:
+      web.debug('Client requested version %s' % label)
+      # Check that matching build exists
+      image_path = '%s/%s' % (self.static_dir, label)
+      if not os.path.exists(image_path):
+        web.debug('%s not found.' % image_path)
+        return self.GetNoUpdatePayload()
+      # Construct a response
+      ok = self.BuildUpdateImage(image_path)
+      if ok != True:
+        web.debug('Failed to build an update image')
+        return self.GetNoUpdatePayload()
+      web.debug('serving update: ')
+      hash = self.GetHash('%s/%s/update.gz' % (self.static_dir, label))
+      size = self.GetSize('%s/%s/update.gz' % (self.static_dir, label))
+      url = 'http://%s/static/archive/%s/update.gz' % (hostname, label)
+      return self.GetUpdatePayload(hash, size, url)
+      web.debug( 'DONE')
+    else:
+      web.debug('update found %s ' % latest_version)
+      ok = self.BuildUpdateImage(latest_image_path)
+      if ok != True:
+        web.debug('Failed to build an update image')
+        return self.GetNoUpdatePayload()
 
+      hash = self.GetHash('%s/update.gz' % self.static_dir)
+      size = self.GetSize('%s/update.gz' % self.static_dir)
+
+      url = 'http://%s/static/update.gz' % hostname
+      return self.GetUpdatePayload(hash, size, url)