blob: 6c0c397c972f762bbaa595ed68024dfc84df0f2a [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright (C) 2020 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# pylint: disable=invalid-name
"""Utility to interact with the Android build API (go/ab)."""
try:
import apiclient.discovery
import apiclient.http
from oauth2client import client as oauth2_client
except ImportError:
missingImportString = """
Missing necessary libraries. Try doing the following:
$ sudo apt-get install python-pip3
$ sudo pip3 install --upgrade google-api-python-client
$ sudo pip3 install --upgrade oauth2client
"""
raise ImportError(missingImportString)
from typing import Tuple, List
import getpass
import io
import logging
from data import WorkNodeRecord, TestResultRecord
import utils
ANDROID_BUILD_API_SCOPE = (
'https://www.googleapis.com/auth/androidbuild.internal')
ANDROID_BUILD_API_NAME = 'androidbuildinternal'
ANDROID_BUILD_API_VERSION = 'v3'
CHUNK_SIZE = 10 * 1024 * 1024 # 10M
STUBBY_COMMAND_PATH = '/google/data/ro/teams/android-llvm/tests/sso_stubby_cmd.sh'
STUBBY_REQUEST = """
target: {{
scope: GAIA_USER
name: "{user}@google.com"
}}
target_credential: {{
type: OAUTH2_TOKEN
oauth2_attributes: {{
scope: '{scope}'
}}
}}
"""
def _get_oauth2_token():
request = STUBBY_REQUEST.format(
user=getpass.getuser(), scope=ANDROID_BUILD_API_SCOPE)
with open(STUBBY_COMMAND_PATH) as stubby_command_file:
stubby_command = stubby_command_file.read().strip().split()
output = utils.check_output(stubby_command, input=request)
# output is of the format:
# oauth2_token: "<TOKEN>"
return output.split('"')[1]
class AndroidBuildClient():
"""Helper class to query the Android build API."""
def __init__(self):
creds = oauth2_client.AccessTokenCredentials(
access_token=_get_oauth2_token(), user_agent='unused/1.0')
self.client = apiclient.discovery.build(
ANDROID_BUILD_API_NAME,
ANDROID_BUILD_API_VERSION,
credentials=creds,
discoveryServiceUrl=apiclient.discovery.DISCOVERY_URI,
cache_discovery=False)
@staticmethod
def _worknode_parse_general(workNodeData):
if 'isFinal' not in workNodeData:
return False, 'Error: isFinal field not present'
isFinal = workNodeData['isFinal']
if not isinstance(isFinal, bool):
return False, 'Error: isFinal expected to be a bool'
if not isFinal:
return False, 'incomplete'
return True, workNodeData['workExecutorType']
def get_worknode_status(self, forrest_invocation_id: str,
tag: str) -> Tuple[bool, List[TestResultRecord]]:
"""Return completion status and results from a Forrest invocation."""
resultStr = lambda res: 'passed' if res else 'failed'
request = self.client.worknode().list(workPlanId=forrest_invocation_id)
response = request.execute()
results = []
workDone = False
for worknode in response['workNodes']:
ok, msg = AndroidBuildClient._worknode_parse_general(worknode)
if not ok:
if msg != 'incomplete':
logging.warning(f'Parsing worknode failed: {msg}\n' +
str(worknode))
continue
if msg == 'trybotFinished':
# Status of trybotFinished worknode tells if work for an
# invocation is completed.
workDone = worknode['status'] == 'complete'
continue
if msg == 'forrestRun':
# Book keeping about a forrest invocation. Safe to ignore.
continue
workOutput = worknode.get('workOutput', None)
success = workOutput and workOutput['success']
if msg == 'pendingChangeBuild':
work_type = 'BUILD'
params = worknode['workParameters']['submitQueue']
# If workOutput is absent, Try to get build Id from
# workParameters.
if workOutput:
build_id = workOutput['buildOutput']['buildId']
elif 'buildIds' in worknode['workParameters']['submitQueue']:
# Just pick the first build Id.
build_id = worknode['workParameters']['submitQueue'][
'buildIds'][0]
else:
build_id = 'NA'
test_name = 'NA'
ants_id = 'NA'
display_message = 'NA'
elif msg == 'atpTest':
work_type = 'TEST'
params = worknode['workParameters']['atpTestParameters']
build_id, ants_id, display_message = 'NA', 'NA', 'NA'
# workOutput may not be available if the test did not run due to
# build failure
if workOutput:
if 'testOutput' in workOutput:
build_id = workOutput['testOutput']['buildId']
ants_id = workOutput['testOutput']['antsInvocationId']
display_message = workOutput.get('displayMessage', 'NA')
test_name = params['testName']
else:
raise RuntimeError(f'Unexpected workExecutorType {msg} with ' +
f'worknode data:\n{worknode}')
branch = params['branch']
target = params['target']
results.append(
TestResultRecord(
tag=tag,
worknode_id=worknode['id'],
work_type=work_type,
branch=branch,
target=target,
build_id=build_id,
result=resultStr(success),
test_name=test_name,
ants_invocation_id=ants_id,
display_message=display_message))
return workDone, results
def get_artifact(self, buildId: str, target: str, resource: str) -> bytes:
"""Download an artifact from the buildbot."""
request = self.client.buildartifact().get_media(
buildId=buildId,
target=target,
attemptId='latest',
resourceId=resource)
stream = io.BytesIO()
try:
downloader = apiclient.http.MediaIoBaseDownload(
stream, request, chunksize=CHUNK_SIZE)
done = False
while not done:
status, done = downloader.next_chunk()
except apiclient.errors.HttpError as e:
logging.error(f'Download failed: {resource} for {buildId}:{target}')
return None
return stream.getvalue()