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