Implement downloading artifacts from Android builds.

BUG=chromium:512668
TEST=local run
download credential file from Cloud console
start devserver:
./devserver.py --static_dir=/usr/local/google/home/dshi/images \
  --log=/tmp/devserver.log --port 8082
  -a "/usr/local/google2/chromiumos/src/platform/dev/android_build_cred.json"
test download:
success:
curl http://127.0.0.1:8082/stage?target=shamu-userdebug\&build_id=2040953\&artifacts=test_zip\&branch=git_mnc-release
fail:
curl http://127.0.0.1:8082/stage?target=shamu-userdebug\&build_id=2040953\&artifacts=test_zip\&branch=none

cros flash a test device

Change-Id: I967d80ed3013f6085ae6d7e56e34b0f9dd9af10e
Reviewed-on: https://chromium-review.googlesource.com/304664
Commit-Ready: Dan Shi <dshi@chromium.org>
Tested-by: Dan Shi <dshi@chromium.org>
Reviewed-by: Simran Basi <sbasi@chromium.org>
diff --git a/android_build.py b/android_build.py
new file mode 100644
index 0000000..cae3b3b
--- /dev/null
+++ b/android_build.py
@@ -0,0 +1,120 @@
+# Copyright 2015 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Helper methods to make Google API call to query Android build server."""
+
+from __future__ import print_function
+
+import apiclient
+import httplib2
+import io
+
+from apiclient import discovery
+from oauth2client.client import SignedJwtAssertionCredentials
+
+CREDENTIAL_SCOPE = 'https://www.googleapis.com/auth/androidbuild.internal'
+DEFAULT_BUILDER = 'androidbuildinternal'
+DEFAULT_CHUNKSIZE = 20*1024*1024
+
+class AndroidBuildFetchError(Exception):
+  """Exception to raise when failed to make calls to Android build server."""
+
+class BuildAccessor(object):
+  """Wrapper class to make Google API call to query Android build server."""
+
+  # Credential information is required to access Android builds. The values will
+  # be set when the devserver starts.
+  credential_info = None
+
+  @classmethod
+  def _GetServiceObject(cls):
+    """Returns a service object with given credential information."""
+    if not cls.credential_info:
+      raise AndroidBuildFetchError('Android Build credential is missing.')
+
+    credentials = SignedJwtAssertionCredentials(
+        cls.credential_info['client_email'],
+        cls.credential_info['private_key'], CREDENTIAL_SCOPE)
+    http_auth = credentials.authorize(httplib2.Http())
+    service_obj = discovery.build(DEFAULT_BUILDER, 'v1', http=http_auth)
+    return service_obj
+
+  @classmethod
+  def _VerifyBranch(cls, service_obj, branch, build_id, target):
+    """Verify the build with given id and target is for the specified branch.
+
+    Args:
+      service_obj: A service object to be used to make API call to build server.
+      branch: branch of the desired build.
+      build_id: Build id of the Android build, e.g., 2155602.
+      target: Target of the Android build, e.g., shamu-userdebug.
+
+    Raises:
+      AndroidBuildFetchError: If the given build id and target are not for the
+                              specified branch.
+    """
+    builds = service_obj.build().list(
+        buildType='submitted', branch=branch, buildId=build_id, target=target,
+        maxResults=0).execute()
+    if not builds:
+      raise AndroidBuildFetchError(
+          'Failed to locate build with branch %s, build id %s and target %s.' %
+          (branch, build_id, target))
+
+  @classmethod
+  def GetArtifacts(cls, branch, build_id, target):
+    """Get the list of artifacts for given build id and target.
+
+    The return value is a list of dictionaries, each containing information
+    about an artifact.
+    For example:
+        {u'contentType': u'application/octet-stream',
+         u'crc32': 4131231264,
+         u'lastModifiedTime': u'143518405786',
+         u'md5': u'c04c823a64293aa5bf508e2eb4683ec8',
+         u'name': u'fastboot',
+         u'revision': u'HsXLpGsgEaqj654THKvR/A==',
+         u'size': u'6999296'},
+
+    Args:
+      branch: branch of the desired build.
+      build_id: Build id of the Android build, e.g., 2155602.
+      target: Target of the Android build, e.g., shamu-userdebug.
+
+    Returns:
+      A list of artifacts for given build id and target.
+    """
+    service_obj = cls._GetServiceObject()
+    cls._VerifyBranch(service_obj, branch, build_id, target)
+
+    # Get all artifacts for the given build_id and target.
+    artifacts = service_obj.buildartifact().list(
+        buildType='submitted', buildId=build_id, target=target,
+        attemptId='latest', maxResults=0).execute()
+    return artifacts['artifacts']
+
+  @classmethod
+  def Download(cls, branch, build_id, target, resource_id, dest_file):
+    """Get the list of artifacts for given build id and target.
+
+    Args:
+      branch: branch of the desired build.
+      build_id: Build id of the Android build, e.g., 2155602.
+      target: Target of the Android build, e.g., shamu-userdebug.
+      resource_id: Name of the artifact to donwload.
+      dest_file: Path to the file to download to.
+    """
+    service_obj = cls._GetServiceObject()
+    cls._VerifyBranch(service_obj, branch, build_id, target)
+
+    # TODO(dshi): Add retry logic here to avoid API flakes.
+    download_req = service_obj.buildartifact().get_media(
+        buildType='submitted', buildId=build_id, target=target,
+        attemptId='latest', resourceId=resource_id)
+    with io.FileIO(dest_file, mode='wb') as fh:
+      downloader = apiclient.http.MediaIoBaseDownload(
+          fh, download_req, chunksize=DEFAULT_CHUNKSIZE)
+      done = None
+      while not done:
+        _, done = downloader.next_chunk()