diff options
Diffstat (limited to 'harnesses/host_controller/build/build_provider_pab.py')
-rw-r--r-- | harnesses/host_controller/build/build_provider_pab.py | 728 |
1 files changed, 0 insertions, 728 deletions
diff --git a/harnesses/host_controller/build/build_provider_pab.py b/harnesses/host_controller/build/build_provider_pab.py deleted file mode 100644 index 9d932b6..0000000 --- a/harnesses/host_controller/build/build_provider_pab.py +++ /dev/null @@ -1,728 +0,0 @@ -# -# Copyright (C) 2017 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. -# -"""Module to fetch artifacts from Partner Android Build server.""" - -import argparse -import getpass -import httplib2 -import json -import logging -import os -import requests -import urlparse -from posixpath import join as path_urljoin - -from oauth2client.client import flow_from_clientsecrets -from oauth2client.file import Storage -from oauth2client.tools import argparser -from oauth2client.tools import run_flow - -from selenium import webdriver -from selenium.webdriver.common.by import By -from selenium.common.exceptions import TimeoutException -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.chrome.options import Options -from selenium.webdriver.support.ui import WebDriverWait - -from host_controller.build import build_provider - -# constants for GET and POST endpoints -GET = 'GET' -POST = 'POST' - -# timeout seconds for requests -REQUESTS_TIMEOUT_SECONDS = 60 - - -class BuildProviderPAB(build_provider.BuildProvider): - """Client that manages Partner Android Build downloading. - - Attributes: - BAD_XSRF_CODE: int, error code for bad XSRF token error - BASE_URL: string, path to PAB entry point - BUILDARTIFACT_NAME_KEY: string, index in artifact containing name - BUILD_BUILDID_KEY: string, index in build containing build_id - BUILD_COMPLETED_STATUS: int, value of 'complete' build - BUILD_STATUS_KEY: string, index in build object containing status. - CHROME_DRIVER_LOCATION: string, path to chromedriver - CHROME_LOCATION: string, path to Chrome browser - CLIENT_STORAGE: string, path to store credentials. - DEFAULT_CHUNK_SIZE: int, number of bytes to download at a time. - DOWNLOAD_URL_KEY: string, index in downloadBuildArtifact containing url - EMAIL: string, email constant for userinfo JSON - EXPIRED_XSRF_CODE: int, error code for expired XSRF token error - GETBUILD_ARTIFACTS_KEY, string, index in build obj containing artifacts - GMS_DOWNLOAD_URL: string, base url for downloading artifacts. - LISTBUILD_BUILD_KEY: string, index in listBuild containing builds - PAB_URL: string, redirect url from Google sign-in to PAB - PASSWORD: string, password constant for userinfo JSON - SCOPE: string, URL for which to request access via oauth2. - SVC_URL: string, path to buildsvc RPC - XSRF_STORE: string, path to store xsrf token - _credentials : oauth2client credentials object - _userinfo_file: location of file containing email and password - _xsrf : string, XSRF token from PAB website. expires after 7 days. - """ - _credentials = None - _userinfo_file = None - _xsrf = None - BAD_XSRF_CODE = -32000 - BASE_URL = 'https://partner.android.com' - BUILDARTIFACT_NAME_KEY = '1' - BUILD_BUILDID_KEY = '1' - BUILD_COMPLETED_STATUS = 7 - BUILD_STATUS_KEY = '7' - CHROME_DRIVER_LOCATION = '/usr/local/bin/chromedriver' - CHROME_LOCATION = '/usr/bin/google-chrome' - CLIENT_SECRETS = os.path.join( - os.path.dirname(__file__), 'client_secrets.json') - CLIENT_STORAGE = os.path.join(os.path.dirname(__file__), 'credentials') - DEFAULT_CHUNK_SIZE = 1024 - DOWNLOAD_URL_KEY = '1' - EMAIL = 'email' - EXPIRED_XSRF_CODE = -32001 - GETBUILD_ARTIFACTS_KEY = '2' - GMS_DOWNLOAD_URL = 'https://partnerdash.google.com/build/gmsdownload' - LISTBUILD_BUILD_KEY = '1' - PAB_URL = ('https://www.google.com/accounts/Login?&continue=' - 'https://partner.android.com/build/') - PASSWORD = 'password' - # need both of these scopes to access PAB downloader - scopes = ('https://www.googleapis.com/auth/partnerdash', - 'https://www.googleapis.com/auth/alkali-base') - SCOPE = ' '.join(scopes) - SVC_URL = urlparse.urljoin(BASE_URL, 'build/u/0/_gwt/_rpc/buildsvc') - XSRF_STORE = os.path.join(os.path.dirname(__file__), 'xsrf') - - def __init__(self): - """Creates a temp dir.""" - super(BuildProviderPAB, self).__init__() - - def Authenticate(self, userinfo_file=None, noauth_local_webserver=False, - scopes=SCOPE): - """Authenticate using OAuth2. - - Args: - userinfo_file: (optional) the path of a JSON file which has - "email" and "password" string fields. - noauth_local_webserver: boolean, True if do not (or can not) use - a local web server. - scopes: string or iterable of strings, the scopes to request. - """ - # this should be a JSON file with "email" and "password" string fields - self._userinfo_file = userinfo_file - logging.info('Parsing flags, use --noauth_local_webserver' - ' if running on remote machine') - - parser = argparse.ArgumentParser(parents=[argparser]) - flags, unknown = parser.parse_known_args() - flags.noauth_local_webserver = noauth_local_webserver - logging.info('Preparing OAuth token') - flow = flow_from_clientsecrets(self.CLIENT_SECRETS, scope=scopes) - storage = Storage(self.CLIENT_STORAGE) - if self._credentials is None: - self._credentials = storage.get() - if self._credentials is None or self._credentials.invalid: - logging.info('Credentials not found, authenticating.') - self._credentials = run_flow(flow, storage, flags) - - if self._credentials.access_token_expired: - logging.info('Access token expired, refreshing.') - self._credentials.refresh(http=httplib2.Http()) - - if self.XSRF_STORE is not None and os.path.isfile(self.XSRF_STORE): - with open(self.XSRF_STORE, 'r') as handle: - self._xsrf = handle.read() - - def GetXSRFToken(self, email=None, password=None): - """Get XSRF token. Prompt if email/password not provided. - - Args: - email: string, optional. Gmail account of user logging in - password: string, optional. Password of user logging in - - Returns: - boolean, whether the token was accessed and stored - - Raises: - ValueError if login fails or userinfo file is malformed. - """ - if self._userinfo_file is not None: - with open(self._userinfo_file, 'r') as handle: - userinfo = json.load(handle) - - if self.EMAIL not in userinfo or self.PASSWORD not in userinfo: - raise ValueError( - 'Malformed userinfo file: needs email and password') - - email = userinfo[self.EMAIL] - password = userinfo[self.PASSWORD] - - chrome_options = Options() - chrome_options.add_argument("--headless") - - driver = webdriver.Chrome( - chrome_options=chrome_options) - - driver.set_window_size(1080, 800) - wait = WebDriverWait(driver, 10) - - driver.get(self.PAB_URL) - - query = driver.find_element_by_id("identifierId") - if email is None: - email = raw_input("Email: ") - query.send_keys(email) - driver.find_element_by_id("identifierNext").click() - - pw = wait.until(EC.element_to_be_clickable((By.NAME, "password"))) - pw.clear() - - if password is None: - pw.send_keys(getpass.getpass("Password: ")) - else: - pw.send_keys(password) - - driver.find_element_by_id("passwordNext").click() - - try: - wait.until(EC.title_contains("Partner Android Build")) - except TimeoutException as e: - logging.exception(e) - raise ValueError('Wrong password or non-standard login flow') - - self._xsrf = driver.execute_script("return clientConfig.XSRF_TOKEN;") - with open(self.XSRF_STORE, 'w') as handle: - handle.write(self._xsrf) - - return True - - def CallBuildsvc(self, method, params, account_id): - """Call the buildsvc RPC with given parameters. - - Args: - method: string, name of method to be called in buildsvc - params: dict, parameters to RPC call - account_id: int, ID associated with the PAB account. - - Returns: - dict, result from RPC call - - Raises: - ValueError if RPC call returns an error or an unknown response. - """ - if self._xsrf is None: - self.GetXSRFToken() - params = json.dumps(params) - - data = {"method": method, "params": params, "xsrf": self._xsrf} - data = json.dumps(data) - headers = {} - self._credentials.apply(headers) - headers['Content-Type'] = 'application/json' - headers['x-alkali-account'] = account_id - - try: - response = requests.post(self.SVC_URL, data=data, headers=headers, - timeout=REQUESTS_TIMEOUT_SECONDS) - except requests.exceptions.Timeout as e: - logging.exception(e) - raise ValueError("Request timeout.") - - responseJSON = {} - - try: - responseJSON = response.json() - except ValueError: - raise ValueError("Backend error -- check your account ID") - - if 'result' in responseJSON: - return responseJSON['result'] - - if 'error' in responseJSON and 'code' in responseJSON['error']: - if responseJSON['error']['code'] == self.BAD_XSRF_CODE: - raise ValueError( - "Bad XSRF token -- must be for the same account as your credentials") - if responseJSON['error']['code'] == self.EXPIRED_XSRF_CODE: - raise ValueError("Expired XSRF token -- please refresh") - - raise ValueError("Unknown response from server -- %s" % - json.dumps(responseJSON)) - - def GetBuildList(self, - account_id, - branch, - target, - page_token="", - max_results=10, - internal=True, - method=GET, - verify_signed=False): - """Get the list of builds for a given account, branch and target - Args: - account_id: int, ID associated with the PAB account. - branch: string, branch to pull resource from. - target: string, "latest" or a specific version. - page_token: string, token used for pagination - max_results: maximum build results the build list contains, e.g. 25 - internal: bool, whether to query internal build - method: 'GET' or 'POST', which endpoint to query - verify_signed: bool, whether to verify signed build. - - Returns: - list of dicts representing the builds, descending in time - - Raises: - ValueError if build request returns an error or builds not found. - """ - if method == POST: - params = { - "1": branch, - "2": target, - "3": page_token, - "4": max_results, - "7": int(internal) - } - - result = self.CallBuildsvc("listBuild", params, account_id) - # in listBuild response, index '1' contains builds - if self.LISTBUILD_BUILD_KEY in result: - return result[self.LISTBUILD_BUILD_KEY] - - if verify_signed: - logging.error("verify_signed does not support POST method.") - - raise ValueError("Build list not found -- %s" % params) - elif method == GET: - headers = {} - self._credentials.apply(headers) - - action = 'list-internal' if internal else 'list' - # PAB URL format expects something (anything) to be given as buildid - # and resource, even for action list - dummy = 'DUMMY' - url = path_urljoin(self.BASE_URL, 'build', 'builds', action, - branch, target, dummy, - dummy) + '?a=' + str(account_id) - try: - response = requests.get(url, headers=headers, - timeout=REQUESTS_TIMEOUT_SECONDS) - responseJSON = response.json() - builds = responseJSON['build'] - except requests.exceptions.Timeout as e: - logging.exception(e) - raise ValueError("Request timeout.") - except ValueError as e: - logging.exception(e) - raise ValueError("Backend error -- check your account ID") - - if verify_signed: - for build in builds: - artifact_name = "signed%2Fsigned-{}-img-{}.zip".format( - target.split("-")[0], build["build_id"]) - logging.debug("Checking whether the build is signed for " - "build_target {} and build_id {}".format( - target, build["build_id"])) - signed_build_url = self.GetArtifactURL( - account_id=account_id, - build_id=build["build_id"], - target=target, - artifact_name=artifact_name, - branch=branch, - internal=False, - method=method) - try: - self.GetResponseWithURL(signed_build_url) - logging.debug("The build is signed.") - build["signed"] = True - except requests.HTTPError: - logging.debug("The build is not signed.") - build["signed"] = False - except requests.exceptions.Timeout as e: - logging.debug("Server is not responding.") - logging.exception(e) - build["signed"] = False - return builds - - def GetLatestBuildId(self, account_id, branch, target, method=GET): - """Get the most recent build_id for a given account, branch and target - Args: - account_id: int, ID associated with the PAB account. - branch: string, branch to pull resource from. - target: string, "latest" or a specific version. - method: 'GET' or 'POST', which endpoint to query - - Returns: - string, most recent build id - - Raises: - ValueError if complete builds are not found. - """ - # TODO: support pagination, maybe? - build_list = self.GetBuildList(account_id=account_id, - branch=branch, - target=target, - method=method) - if len(build_list) == 0: - raise ValueError( - 'No builds found for account_id=%s, branch=%s, target=%s' % - (account_id, branch, target)) - for build in build_list: - if method == POST: - # get build status: 7 = completed build - if build.get(self.BUILD_STATUS_KEY, - None) == self.BUILD_COMPLETED_STATUS: - # return build id (index '1') - return build[self.BUILD_BUILDID_KEY] - elif method == GET: - if build['build_attempt_status'] == "COMPLETE" and build[ - "successful"]: - return build['build_id'] - raise ValueError( - 'No complete builds found: %s failed or incomplete builds found' % - len(build_list)) - - def GetBuildArtifacts( - self, account_id, build_id, branch, target, method=POST): - """Get the list of build artifacts. - - For an account, build, target, branch. - - Args: - account_id: int, ID associated with the PAB account. - build_id: string, ID of the build - branch: string, branch to pull resource from. - target: string, "latest" or a specific version. - method: 'GET' or 'POST', which endpoint to query - - Returns: - list of build artifact objects - - Raises: - NotImplementedError if method is 'GET', which is not supported yet. - ValueError if build artifacts are not found. - """ - if method == GET: - raise NotImplementedError( - "GetBuildArtifacts not supported with GET") - params = {"1": build_id, "2": target, "3": branch} - - result = self.CallBuildsvc("getBuild", params, account_id) - # in getBuild response, index '2' contains the artifacts - if self.GETBUILD_ARTIFACTS_KEY in result: - return result[self.GETBUILD_ARTIFACTS_KEY] - if len(result) == 0: - raise ValueError("Build artifacts not found -- %s" % params) - - def GetArtifactURL(self, - account_id, - build_id, - target, - artifact_name, - branch, - internal, - method=GET): - """Get the URL for an artifact on the PAB server, using buildsvc. - - Args: - account_id: int, ID associated with the PAB account. - build_id: string/int, id of the build. - target: string, "latest" or a specific version. - artifact_name: string, simple file name (no parent dir or path). - branch: string, branch to pull resource from. - internal: int, whether the request is for an internal build artifact - method: 'GET' or 'POST', which endpoint to query - - Returns: - string, The URL for the resource specified by the parameters - - Raises: - ValueError if given parameters are incorrect or resource not found. - """ - if method == POST: - params = { - "1": str(build_id), - "2": target, - "3": artifact_name, - "4": branch, - "5": "", # release_candidate_name - "6": internal - } - - result = self.CallBuildsvc(method='downloadBuildArtifact', - params=params, - account_id=account_id) - - # in downloadBuildArtifact response, index '1' contains the url - if self.DOWNLOAD_URL_KEY in result: - return result[self.DOWNLOAD_URL_KEY] - if len(result) == 0: - raise ValueError("Resource not found -- %s" % params) - elif method == GET: - headers = {} - self._credentials.apply(headers) - - action = 'get-internal' if internal else 'get' - get_url = path_urljoin(self.BASE_URL, 'build', 'builds', action, - branch, target, build_id, - artifact_name) + '?a=' + str(account_id) - - try: - response = requests.get(get_url, headers=headers, - timeout=REQUESTS_TIMEOUT_SECONDS) - responseJSON = response.json() - return responseJSON['url'] - except requests.exceptions.Timeout as e: - logging.exception(e) - raise ValueError("Request timeout.") - except ValueError: - raise ValueError("Backend error -- check your account ID") - - def DownloadArtifact(self, download_url, filename): - """Get artifact from Partner Android Build server. - - Args: - download_url: location of resource that we want to download - filename: where the artifact gets downloaded locally. - - Returns: - boolean, whether the file was successfully downloaded - """ - try: - response = self.GetResponseWithURL(download_url) - except (requests.HTTPError, requests.exceptions.Timeout) as error: - logging.exception(error) - return False - logging.info('%s now downloading...', download_url) - with open(filename, 'wb') as handle: - for block in response.iter_content(self.DEFAULT_CHUNK_SIZE): - handle.write(block) - return True - - def GetArtifact(self, - account_id, - branch, - target, - artifact_name, - build_id='latest', - method=GET, - full_device_images=False): - """Get an artifact for an account, branch, target and name and build id. - - If build_id not given, get latest. - - Args: - account_id: int, ID associated with the PAB account. - branch: string, branch to pull resource from. - target: string, "latest" or a specific version. - artifact_name: name of artifact, e.g. aosp_arm64_ab-img-4353141.zip - ({id} will automatically get replaced with build ID) - build_id: string, build ID of an artifact to fetch (or 'latest'). - method: 'GET' or 'POST', which endpoint to query. - - Returns: - a dict containing the device image info. - a dict containing the test suite package info. - a dict containing the artifact info. - a dict containing the global config info. - - Raises: - ValueError if artifacts are not found. - """ - artifact_info = {} - if build_id == 'latest': - build_id = self.GetLatestBuildId(account_id=account_id, - branch=branch, - target=target, - method=method) - logging.info("latest build ID = %s", build_id) - artifact_info["build_id"] = build_id - - if "build_id" in artifact_name: - artifact_name = artifact_name.format(build_id=build_id) - - if method == POST: - artifacts = self.GetBuildArtifacts(account_id=account_id, - build_id=build_id, - branch=branch, - target=target, - method=method) - - if len(artifacts) == 0: - raise ValueError( - "No artifacts found for build_id=%s, branch=%s, target=%s" - % (build_id, branch, target)) - - # in build artifact response, index '1' contains the name - artifact_names = [ - artifact[self.BUILDARTIFACT_NAME_KEY] for artifact in artifacts - ] - if artifact_name not in artifact_names: - raise ValueError("%s not found in artifact list" % - artifact_name) - - url = self.GetArtifactURL(account_id=account_id, - build_id=build_id, - target=target, - artifact_name=artifact_name, - branch=branch, - internal=False, - method=method) - - if self.tmp_dirpath: - artifact_path = os.path.join(self.tmp_dirpath, artifact_name) - else: - artifact_path = artifact_name - self.DownloadArtifact(url, artifact_path) - - self.SetFetchedFile( - artifact_path, full_device_images=full_device_images) - - return (self.GetDeviceImage(), self.GetTestSuitePackage(), - artifact_info, self.GetConfigPackage()) - - def GetSignedBuildArtifact(self, - account_id, - branch, - target, - artifact_name, - build_id='latest', - method=GET, - full_device_images=False): - """Get an signed build artifact from the PAB bulid list. - - Args: - account_id: int, ID associated with the PAB account. - branch: string, branch to pull resource from. - target: string, "latest" or a specific version. - artifact_name: name of artifact, e.g. aosp_arm64_ab-img-4353141.zip - ({id} will automatically get replaced with build ID) - build_id: string, build ID of an artifact to fetch (or 'latest'). - method: 'GET' or 'POST', which endpoint to query. - - Returns: - a dict containing the device image info. - a dict containing the test suite package info. - a dict containing the artifact info. - a dict containing the global config info. - """ - artifact_info = {} - build_ids = [] - artifact_path = "" - if build_id == 'latest': - try: - build_list = self.GetBuildList( - account_id=account_id, - branch=branch, - target=target, - method=method) - for build in build_list: - build_ids.append(build["build_id"]) - except ValueError as e: - logging.exception(e) - else: - build_ids.append(build_id) - - for build_id in build_ids: - _artifact_name = artifact_name - if "build_id" in _artifact_name: - _artifact_name = _artifact_name.format(build_id=build_id) - _artifact_name = "signed%2Fsigned-" + _artifact_name - try: - url = self.GetArtifactURL( - account_id=account_id, - build_id=build_id, - target=target, - artifact_name=_artifact_name, - branch=branch, - internal=False, - method=method) - except ValueError as e: - logging.exception(e) - continue - - if self.tmp_dirpath: - artifact_path = os.path.join(self.tmp_dirpath, _artifact_name) - else: - artifact_path = _artifact_name - ret = self.DownloadArtifact(url, artifact_path) - - if ret: - artifact_info["build_id"] = build_id - break - - self.SetFetchedFile( - artifact_path, full_device_images=full_device_images) - - return (self.GetDeviceImage(), self.GetTestSuitePackage(), - artifact_info, self.GetConfigPackage()) - - def GetResponseWithURL(self, url): - """Gets the response content from the server connected with the url. - - Args: - url: A string representing the server url. - - Returns: - A Response object received from the server. - - Raises: - requests.HTTPError if response.status_code is not 200. - requests.exceptions.Timeout if the server does not respond. - """ - headers = {} - self._credentials.apply(headers) - response = requests.get(url, headers=headers, stream=True, - timeout=REQUESTS_TIMEOUT_SECONDS) - response.raise_for_status() - - return response - - def FetchLatestBuiltHCPackage(self, account_id, branch, target): - """Fetchs the latest <artifact_name> file and return the path. - - Args: - account_id: string, Partner Android Build account_id to use. - branch: string, branch to grab the artifact from. - targets: string, a comma-separate list of build target product(s). - - Returns: - path to the fetched file. None if the fetching has failed. - """ - try: - listed_builds = self.GetBuildList( - account_id=account_id, - branch=branch, - target=target, - page_token="", - max_results=1, - method="GET") - - if listed_builds and len(listed_builds) > 0: - for listed_build in listed_builds: - if listed_build["successful"]: - self.GetArtifact( - account_id=account_id, - branch=branch, - target=target, - artifact_name="android-vtslab.zip", - build_id=listed_build["build_id"], - method="GET") - - return self.GetHostControllerPackage("vtslab") - except ValueError as e: - logging.exception(e) |