devserver: stage images using image archive uploaded to Google Storage

This adds a devserver feature that'll download an stage individual
images from the builders' Google Storage archive
(gs://chromeos-image-archive).

- Defines a new CherryPy URL (stage_images) and hooks it to the staging
  code.

- Implements a downloader for the image archive zipfile (image.zip),
  which will fetch it to a temporary directory and unzip the desired
  images to the target (staging) directory.

- Uses a flag file for recording the set of images that have already
  been staged for a given build.

BUG=chromium-os:33762
TEST=devserver stages requested image files, which can be served in
subsequent requests.

Change-Id: I339bd0edb1511c68cca36378c189551511ad1639
Reviewed-on: https://gerrit.chromium.org/gerrit/33140
Commit-Ready: Gilad Arnold <garnold@chromium.org>
Reviewed-by: Gilad Arnold <garnold@chromium.org>
Tested-by: Gilad Arnold <garnold@chromium.org>
diff --git a/downloadable_artifact.py b/downloadable_artifact.py
index 9251c43..7a965cb 100644
--- a/downloadable_artifact.py
+++ b/downloadable_artifact.py
@@ -21,6 +21,7 @@
 AUTOTEST_PACKAGE = 'autotest.tar'
 AUTOTEST_ZIPPED_PACKAGE = 'autotest.tar.bz2'
 TEST_SUITES_PACKAGE = 'test_suites.tar.bz2'
+IMAGE_ARCHIVE = 'image.zip'
 
 
 class ArtifactDownloadError(Exception):
@@ -165,3 +166,42 @@
       subprocess.check_call(cmd, shell=True)
     except subprocess.CalledProcessError, e:
       raise ArtifactDownloadError('%s %s' % (msg, e))
+
+
+class Zipfile(DownloadableArtifact):
+  """A downloadable artifact that is a zipfile.
+
+  This class defines an extra public method for setting the list of files to be
+  extracted upon staging. Staging amounts to unzipping the desired files to the
+  install path.
+
+  """
+
+  def __init__(self, gs_path, tmp_staging_dir, install_path, synchronous=False,
+               unzip_file_list=None):
+    super(Zipfile, self).__init__(
+        gs_path, tmp_staging_dir, install_path, synchronous)
+    self._unzip_file_list = unzip_file_list
+
+  def _Unzip(self):
+    """Unzip files into the install path."""
+
+    cmd = 'unzip -o %s -d %s%s' % (
+        self._tmp_stage_path,
+        os.path.join(self._install_path),
+        (' ' + ' '.join(self._unzip_file_list)
+         if self._unzip_file_list else ''))
+    cherrypy.log('unzip command: %s' % cmd)
+    msg = 'An error occurred when attempting to unzip %s' % self._tmp_stage_path
+
+    try:
+      subprocess.check_call(cmd, shell=True)
+    except subprocess.CalledProcessError, e:
+      raise ArtifactDownloadError('%s %s' % (msg, e))
+
+  def Stage(self):
+    """Unzip files into the install path."""
+    if not os.path.isdir(self._install_path):
+      os.makedirs(self._install_path)
+
+    self._Unzip()