Dan Shi | 72b1613 | 2015-10-08 19:10:33 | [diff] [blame] | 1 | # Copyright 2015 The Chromium OS Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
| 5 | """Helper methods to make Google API call to query Android build server.""" |
| 6 | |
| 7 | from __future__ import print_function |
| 8 | |
| 9 | import apiclient |
| 10 | import httplib2 |
| 11 | import io |
Dan Shi | 7b9b6a9 | 2015-11-12 09:00:29 | [diff] [blame] | 12 | import subprocess |
Dan Shi | 72b1613 | 2015-10-08 19:10:33 | [diff] [blame] | 13 | |
| 14 | from apiclient import discovery |
| 15 | from oauth2client.client import SignedJwtAssertionCredentials |
| 16 | |
Dan Shi | 7b9b6a9 | 2015-11-12 09:00:29 | [diff] [blame] | 17 | import retry |
| 18 | |
| 19 | |
Dan Shi | 72b1613 | 2015-10-08 19:10:33 | [diff] [blame] | 20 | CREDENTIAL_SCOPE = 'https://www.googleapis.com/auth/androidbuild.internal' |
| 21 | DEFAULT_BUILDER = 'androidbuildinternal' |
| 22 | DEFAULT_CHUNKSIZE = 20*1024*1024 |
Dan Shi | 7b9b6a9 | 2015-11-12 09:00:29 | [diff] [blame] | 23 | # Maximum attempts to interact with Launch Control API. |
| 24 | MAX_ATTEMPTS = 10 |
| 25 | # Timeout in minutes for downloading attempt. |
| 26 | DOWNLOAD_TIMEOUT_MINS = 30 |
| 27 | # Timeout in minutes for API query. |
| 28 | QUERY_TIMEOUT_MINS = 1 |
| 29 | |
Dan Shi | 72b1613 | 2015-10-08 19:10:33 | [diff] [blame] | 30 | |
| 31 | class AndroidBuildFetchError(Exception): |
| 32 | """Exception to raise when failed to make calls to Android build server.""" |
| 33 | |
| 34 | class BuildAccessor(object): |
| 35 | """Wrapper class to make Google API call to query Android build server.""" |
| 36 | |
| 37 | # Credential information is required to access Android builds. The values will |
| 38 | # be set when the devserver starts. |
| 39 | credential_info = None |
| 40 | |
| 41 | @classmethod |
Dan Shi | 7b9b6a9 | 2015-11-12 09:00:29 | [diff] [blame] | 42 | @retry.retry(Exception, timeout_min=QUERY_TIMEOUT_MINS) |
Dan Shi | 72b1613 | 2015-10-08 19:10:33 | [diff] [blame] | 43 | def _GetServiceObject(cls): |
| 44 | """Returns a service object with given credential information.""" |
| 45 | if not cls.credential_info: |
| 46 | raise AndroidBuildFetchError('Android Build credential is missing.') |
| 47 | |
| 48 | credentials = SignedJwtAssertionCredentials( |
| 49 | cls.credential_info['client_email'], |
| 50 | cls.credential_info['private_key'], CREDENTIAL_SCOPE) |
| 51 | http_auth = credentials.authorize(httplib2.Http()) |
Dan Shi | 7b9b6a9 | 2015-11-12 09:00:29 | [diff] [blame] | 52 | return discovery.build(DEFAULT_BUILDER, 'v1', http=http_auth) |
| 53 | |
Dan Shi | cfd9aac | 2016-05-18 16:52:28 | [diff] [blame] | 54 | @staticmethod |
| 55 | def _GetBuildType(build_id): |
| 56 | """Get the build type based on the given build id. |
| 57 | |
| 58 | Args: |
| 59 | build_id: Build id of the Android build, e.g., 2155602. |
| 60 | |
| 61 | Returns: |
| 62 | The build type, e.g., submitted, pending. |
| 63 | """ |
| 64 | if build_id and build_id.lower().startswith('p'): |
| 65 | return 'pending' |
| 66 | return 'submitted' |
Dan Shi | 72b1613 | 2015-10-08 19:10:33 | [diff] [blame] | 67 | |
| 68 | @classmethod |
| 69 | def _VerifyBranch(cls, service_obj, branch, build_id, target): |
| 70 | """Verify the build with given id and target is for the specified branch. |
| 71 | |
| 72 | Args: |
| 73 | service_obj: A service object to be used to make API call to build server. |
| 74 | branch: branch of the desired build. |
| 75 | build_id: Build id of the Android build, e.g., 2155602. |
| 76 | target: Target of the Android build, e.g., shamu-userdebug. |
| 77 | |
| 78 | Raises: |
| 79 | AndroidBuildFetchError: If the given build id and target are not for the |
| 80 | specified branch. |
| 81 | """ |
Dan Shi | cfd9aac | 2016-05-18 16:52:28 | [diff] [blame] | 82 | build_type = cls._GetBuildType(build_id) |
Dan Shi | 72b1613 | 2015-10-08 19:10:33 | [diff] [blame] | 83 | builds = service_obj.build().list( |
Dan Shi | cfd9aac | 2016-05-18 16:52:28 | [diff] [blame] | 84 | buildType=build_type, branch=branch, buildId=build_id, target=target, |
Dan Shi | 7b9b6a9 | 2015-11-12 09:00:29 | [diff] [blame] | 85 | maxResults=0).execute(num_retries=MAX_ATTEMPTS) |
Dan Shi | 72b1613 | 2015-10-08 19:10:33 | [diff] [blame] | 86 | if not builds: |
| 87 | raise AndroidBuildFetchError( |
| 88 | 'Failed to locate build with branch %s, build id %s and target %s.' % |
| 89 | (branch, build_id, target)) |
| 90 | |
| 91 | @classmethod |
| 92 | def GetArtifacts(cls, branch, build_id, target): |
| 93 | """Get the list of artifacts for given build id and target. |
| 94 | |
| 95 | The return value is a list of dictionaries, each containing information |
| 96 | about an artifact. |
| 97 | For example: |
| 98 | {u'contentType': u'application/octet-stream', |
| 99 | u'crc32': 4131231264, |
| 100 | u'lastModifiedTime': u'143518405786', |
| 101 | u'md5': u'c04c823a64293aa5bf508e2eb4683ec8', |
| 102 | u'name': u'fastboot', |
| 103 | u'revision': u'HsXLpGsgEaqj654THKvR/A==', |
| 104 | u'size': u'6999296'}, |
| 105 | |
| 106 | Args: |
| 107 | branch: branch of the desired build. |
| 108 | build_id: Build id of the Android build, e.g., 2155602. |
| 109 | target: Target of the Android build, e.g., shamu-userdebug. |
| 110 | |
| 111 | Returns: |
| 112 | A list of artifacts for given build id and target. |
| 113 | """ |
| 114 | service_obj = cls._GetServiceObject() |
| 115 | cls._VerifyBranch(service_obj, branch, build_id, target) |
Dan Shi | cfd9aac | 2016-05-18 16:52:28 | [diff] [blame] | 116 | build_type = cls._GetBuildType(build_id) |
Dan Shi | 72b1613 | 2015-10-08 19:10:33 | [diff] [blame] | 117 | |
| 118 | # Get all artifacts for the given build_id and target. |
Dan Shi | 04049bc | 2017-06-02 19:11:36 | [diff] [blame] | 119 | artifacts = [] |
| 120 | req = service_obj.buildartifact().list( |
Dan Shi | cfd9aac | 2016-05-18 16:52:28 | [diff] [blame] | 121 | buildType=build_type, buildId=build_id, target=target, |
Dan Shi | 04049bc | 2017-06-02 19:11:36 | [diff] [blame] | 122 | attemptId='latest', maxResults=10) |
| 123 | while req: |
| 124 | response = req.execute(num_retries=MAX_ATTEMPTS) |
| 125 | if not response: |
| 126 | break |
| 127 | artifacts.extend(response.get('artifacts', [])) |
| 128 | req = service_obj.buildartifact().list_next(req, response) |
| 129 | |
| 130 | return artifacts |
Dan Shi | 72b1613 | 2015-10-08 19:10:33 | [diff] [blame] | 131 | |
| 132 | @classmethod |
Dan Shi | 7b9b6a9 | 2015-11-12 09:00:29 | [diff] [blame] | 133 | @retry.retry(Exception, timeout_min=DOWNLOAD_TIMEOUT_MINS) |
Dan Shi | 72b1613 | 2015-10-08 19:10:33 | [diff] [blame] | 134 | def Download(cls, branch, build_id, target, resource_id, dest_file): |
Dan Shi | 7b9b6a9 | 2015-11-12 09:00:29 | [diff] [blame] | 135 | """Download the list of artifacts for given build id and target. |
Dan Shi | 72b1613 | 2015-10-08 19:10:33 | [diff] [blame] | 136 | |
| 137 | Args: |
| 138 | branch: branch of the desired build. |
| 139 | build_id: Build id of the Android build, e.g., 2155602. |
| 140 | target: Target of the Android build, e.g., shamu-userdebug. |
| 141 | resource_id: Name of the artifact to donwload. |
| 142 | dest_file: Path to the file to download to. |
| 143 | """ |
| 144 | service_obj = cls._GetServiceObject() |
| 145 | cls._VerifyBranch(service_obj, branch, build_id, target) |
| 146 | |
Dan Shi | 7b9b6a9 | 2015-11-12 09:00:29 | [diff] [blame] | 147 | # Delete partially downloaded file if exists. |
| 148 | subprocess.call(['rm', '-rf', dest_file]) |
| 149 | |
Dan Shi | cfd9aac | 2016-05-18 16:52:28 | [diff] [blame] | 150 | build_type = cls._GetBuildType(build_id) |
Dan Shi | 72b1613 | 2015-10-08 19:10:33 | [diff] [blame] | 151 | # TODO(dshi): Add retry logic here to avoid API flakes. |
| 152 | download_req = service_obj.buildartifact().get_media( |
Dan Shi | cfd9aac | 2016-05-18 16:52:28 | [diff] [blame] | 153 | buildType=build_type, buildId=build_id, target=target, |
Dan Shi | 72b1613 | 2015-10-08 19:10:33 | [diff] [blame] | 154 | attemptId='latest', resourceId=resource_id) |
| 155 | with io.FileIO(dest_file, mode='wb') as fh: |
| 156 | downloader = apiclient.http.MediaIoBaseDownload( |
| 157 | fh, download_req, chunksize=DEFAULT_CHUNKSIZE) |
| 158 | done = None |
| 159 | while not done: |
Dan Shi | 7b9b6a9 | 2015-11-12 09:00:29 | [diff] [blame] | 160 | _, done = downloader.next_chunk(num_retries=MAX_ATTEMPTS) |
| 161 | |
Dan Shi | 61305df | 2015-10-26 23:52:35 | [diff] [blame] | 162 | |
| 163 | @classmethod |
Dan Shi | 7b9b6a9 | 2015-11-12 09:00:29 | [diff] [blame] | 164 | @retry.retry(Exception, timeout_min=QUERY_TIMEOUT_MINS, |
| 165 | blacklist=[AndroidBuildFetchError]) |
Dan Shi | 61305df | 2015-10-26 23:52:35 | [diff] [blame] | 166 | def GetLatestBuildID(cls, target, branch): |
| 167 | """Get the latest build ID for the given target and branch. |
| 168 | |
| 169 | Args: |
| 170 | branch: branch of the desired build. |
| 171 | target: Target of the Android build, e.g., shamu-userdebug. |
| 172 | |
| 173 | Returns: |
| 174 | Build id of the latest successful Android build for the given target and |
| 175 | branch, e.g., 2155602. |
| 176 | """ |
| 177 | service_obj = cls._GetServiceObject() |
| 178 | builds = service_obj.build().list( |
| 179 | buildType='submitted', branch=branch, target=target, successful=True, |
Dan Shi | 7b9b6a9 | 2015-11-12 09:00:29 | [diff] [blame] | 180 | maxResults=1).execute(num_retries=MAX_ATTEMPTS) |
Dan Shi | 61305df | 2015-10-26 23:52:35 | [diff] [blame] | 181 | if not builds or not builds['builds']: |
| 182 | raise AndroidBuildFetchError( |
| 183 | 'Failed to locate build with branch %s and target %s.' % |
| 184 | (branch, target)) |
| 185 | return builds['builds'][0]['buildId'] |