1#!/usr/bin/env python 2# 3# Copyright 2016 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17"""A client that talks to Android Build APIs.""" 18 19import collections 20import io 21import logging 22 23import apiclient 24 25from acloud import errors 26from acloud.internal.lib import base_cloud_client 27 28 29logger = logging.getLogger(__name__) 30 31# The BuildInfo namedtuple data structure. 32# It will be the data structure returned by GetBuildInfo method. 33BuildInfo = collections.namedtuple("BuildInfo", [ 34 "branch", # The branch name string 35 "build_id", # The build id string 36 "build_target", # The build target string 37 "release_build_id"]) # The release build id string 38 39 40class AndroidBuildClient(base_cloud_client.BaseCloudApiClient): 41 """Client that manages Android Build.""" 42 43 # API settings, used by BaseCloudApiClient. 44 API_NAME = "androidbuildinternal" 45 API_VERSION = "v2beta1" 46 SCOPE = "https://www.googleapis.com/auth/androidbuild.internal" 47 48 # other variables. 49 DEFAULT_RESOURCE_ID = "0" 50 # TODO(b/27269552): We should use "latest". 51 DEFAULT_ATTEMPT_ID = "0" 52 DEFAULT_CHUNK_SIZE = 20 * 1024 * 1024 53 NO_ACCESS_ERROR_PATTERN = "does not have storage.objects.create access" 54 # LKGB variables. 55 BUILD_STATUS_COMPLETE = "complete" 56 BUILD_TYPE_SUBMITTED = "submitted" 57 ONE_RESULT = 1 58 BUILD_SUCCESSFUL = True 59 LATEST = "latest" 60 61 # Message constant 62 COPY_TO_MSG = ("build artifact (target: %s, build_id: %s, " 63 "artifact: %s, attempt_id: %s) to " 64 "google storage (bucket: %s, path: %s)") 65 # pylint: disable=invalid-name 66 def DownloadArtifact(self, 67 build_target, 68 build_id, 69 resource_id, 70 local_dest, 71 attempt_id=None): 72 """Get Android build attempt information. 73 74 Args: 75 build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug" 76 build_id: Build id, a string, e.g. "2263051", "P2804227" 77 resource_id: Id of the resource, e.g "avd-system.tar.gz". 78 local_dest: A local path where the artifact should be stored. 79 e.g. "/tmp/avd-system.tar.gz" 80 attempt_id: String, attempt id, will default to DEFAULT_ATTEMPT_ID. 81 """ 82 attempt_id = attempt_id or self.DEFAULT_ATTEMPT_ID 83 api = self.service.buildartifact().get_media( 84 buildId=build_id, 85 target=build_target, 86 attemptId=attempt_id, 87 resourceId=resource_id) 88 logger.info("Downloading artifact: target: %s, build_id: %s, " 89 "resource_id: %s, dest: %s", build_target, build_id, 90 resource_id, local_dest) 91 try: 92 with io.FileIO(local_dest, mode="wb") as fh: 93 downloader = apiclient.http.MediaIoBaseDownload( 94 fh, api, chunksize=self.DEFAULT_CHUNK_SIZE) 95 done = False 96 while not done: 97 _, done = downloader.next_chunk() 98 logger.info("Downloaded artifact: %s", local_dest) 99 except (OSError, apiclient.errors.HttpError) as e: 100 logger.error("Downloading artifact failed: %s", str(e)) 101 raise errors.DriverError(str(e)) 102 103 def CopyTo(self, 104 build_target, 105 build_id, 106 artifact_name, 107 destination_bucket, 108 destination_path, 109 attempt_id=None): 110 """Copy an Android Build artifact to a storage bucket. 111 112 Args: 113 build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug" 114 build_id: Build id, a string, e.g. "2263051", "P2804227" 115 artifact_name: Name of the artifact, e.g "avd-system.tar.gz". 116 destination_bucket: String, a google storage bucket name. 117 destination_path: String, "path/inside/bucket" 118 attempt_id: String, attempt id, will default to DEFAULT_ATTEMPT_ID. 119 """ 120 attempt_id = attempt_id or self.DEFAULT_ATTEMPT_ID 121 copy_msg = "Copying %s" % self.COPY_TO_MSG 122 logger.info(copy_msg, build_target, build_id, artifact_name, 123 attempt_id, destination_bucket, destination_path) 124 api = self.service.buildartifact().copyTo( 125 buildId=build_id, 126 target=build_target, 127 attemptId=attempt_id, 128 artifactName=artifact_name, 129 destinationBucket=destination_bucket, 130 destinationPath=destination_path) 131 try: 132 self.Execute(api) 133 finish_msg = "Finished copying %s" % self.COPY_TO_MSG 134 logger.info(finish_msg, build_target, build_id, artifact_name, 135 attempt_id, destination_bucket, destination_path) 136 except errors.HttpError as e: 137 if e.code == 503: 138 if self.NO_ACCESS_ERROR_PATTERN in str(e): 139 error_msg = "Please grant android build team's service account " 140 error_msg += "write access to bucket %s. Original error: %s" 141 error_msg %= (destination_bucket, str(e)) 142 raise errors.HttpError(e.code, message=error_msg) 143 raise 144 145 def GetBranch(self, build_target, build_id): 146 """Derives branch name. 147 148 Args: 149 build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug" 150 build_id: Build ID, a string, e.g. "2263051", "P2804227" 151 152 Returns: 153 A string, the name of the branch 154 """ 155 api = self.service.build().get(buildId=build_id, target=build_target) 156 build = self.Execute(api) 157 return build.get("branch", "") 158 159 def GetLKGB(self, build_target, build_branch): 160 """Get latest successful build id. 161 162 From branch and target, we can use api to query latest successful build id. 163 e.g. {u'nextPageToken':..., u'builds': [{u'completionTimestamp':u'1534157869286', 164 ... u'buildId': u'4949805', u'machineName'...}]} 165 166 Args: 167 build_target: String, target name, e.g. "aosp_cf_x86_phone-userdebug" 168 build_branch: String, git branch name, e.g. "aosp-master" 169 170 Returns: 171 A string, string of build id number. 172 173 Raises: 174 errors.CreateError: Can't get build id. 175 """ 176 api = self.service.build().list( 177 branch=build_branch, 178 target=build_target, 179 buildAttemptStatus=self.BUILD_STATUS_COMPLETE, 180 buildType=self.BUILD_TYPE_SUBMITTED, 181 maxResults=self.ONE_RESULT, 182 successful=self.BUILD_SUCCESSFUL) 183 build = self.Execute(api) 184 logger.info("GetLKGB build API response: %s", build) 185 if build: 186 return str(build.get("builds")[0].get("buildId")) 187 raise errors.GetBuildIDError( 188 "No available good builds for branch: %s target: %s" 189 % (build_branch, build_target) 190 ) 191 192 def GetBuildInfo(self, build_target, build_id, branch): 193 """Get build info namedtuple. 194 195 Args: 196 build_target: Target name. 197 build_id: Build id, a string or None, e.g. "2263051", "P2804227" 198 If None or latest, the last green build id will be 199 returned. 200 branch: Branch name, a string or None, e.g. git_master. If None, the 201 returned branch will be searched by given build_id. 202 203 Returns: 204 A build info namedtuple with keys build_target, build_id, branch and 205 gcs_bucket_build_id 206 """ 207 if build_id and build_id != self.LATEST: 208 # Get build from build_id and build_target 209 api = self.service.build().get(buildId=build_id, 210 target=build_target) 211 build = self.Execute(api) or {} 212 elif branch: 213 # Get last green build in the branch 214 api = self.service.build().list( 215 branch=branch, 216 target=build_target, 217 successful=True, 218 maxResults=1, 219 buildType="submitted") 220 builds = self.Execute(api).get("builds", []) 221 build = builds[0] if builds else {} 222 else: 223 build = {} 224 225 build_id = build.get("buildId") 226 build_target = build_target if build_id else None 227 return BuildInfo(build.get("branch"), build_id, build_target, 228 build.get("releaseCandidateName")) 229