1#
2# Copyright (C) 2017 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16"""Module to fetch artifacts from Partner Android Build server."""
17
18import argparse
19import getpass
20import httplib2
21import json
22import logging
23import os
24import requests
25import urlparse
26from posixpath import join as path_urljoin
27
28from oauth2client.client import flow_from_clientsecrets
29from oauth2client.file import Storage
30from oauth2client.tools import argparser
31from oauth2client.tools import run_flow
32
33from selenium import webdriver
34from selenium.webdriver.common.by import By
35from selenium.common.exceptions import TimeoutException
36from selenium.webdriver.support import expected_conditions as EC
37from selenium.webdriver.common.keys import Keys
38from selenium.webdriver.chrome.options import Options
39from selenium.webdriver.support.ui import WebDriverWait
40
41from host_controller.build import build_provider
42
43# constants for GET and POST endpoints
44GET = 'GET'
45POST = 'POST'
46
47# timeout seconds for requests
48REQUESTS_TIMEOUT_SECONDS = 60
49
50
51class BuildProviderPAB(build_provider.BuildProvider):
52    """Client that manages Partner Android Build downloading.
53
54    Attributes:
55        BAD_XSRF_CODE: int, error code for bad XSRF token error
56        BASE_URL: string, path to PAB entry point
57        BUILDARTIFACT_NAME_KEY: string, index in artifact containing name
58        BUILD_BUILDID_KEY: string, index in build containing build_id
59        BUILD_COMPLETED_STATUS: int, value of 'complete' build
60        BUILD_STATUS_KEY: string, index in build object containing status.
61        CHROME_DRIVER_LOCATION: string, path to chromedriver
62        CHROME_LOCATION: string, path to Chrome browser
63        CLIENT_STORAGE: string, path to store credentials.
64        DEFAULT_CHUNK_SIZE: int, number of bytes to download at a time.
65        DOWNLOAD_URL_KEY: string, index in downloadBuildArtifact containing url
66        EMAIL: string, email constant for userinfo JSON
67        EXPIRED_XSRF_CODE: int, error code for expired XSRF token error
68        GETBUILD_ARTIFACTS_KEY, string, index in build obj containing artifacts
69        GMS_DOWNLOAD_URL: string, base url for downloading artifacts.
70        LISTBUILD_BUILD_KEY: string, index in listBuild containing builds
71        PAB_URL: string, redirect url from Google sign-in to PAB
72        PASSWORD: string, password constant for userinfo JSON
73        SCOPE: string, URL for which to request access via oauth2.
74        SVC_URL: string, path to buildsvc RPC
75        XSRF_STORE: string, path to store xsrf token
76        _credentials : oauth2client credentials object
77        _userinfo_file: location of file containing email and password
78        _xsrf : string, XSRF token from PAB website. expires after 7 days.
79    """
80    _credentials = None
81    _userinfo_file = None
82    _xsrf = None
83    BAD_XSRF_CODE = -32000
84    BASE_URL = 'https://partner.android.com'
85    BUILDARTIFACT_NAME_KEY = '1'
86    BUILD_BUILDID_KEY = '1'
87    BUILD_COMPLETED_STATUS = 7
88    BUILD_STATUS_KEY = '7'
89    CHROME_DRIVER_LOCATION = '/usr/local/bin/chromedriver'
90    CHROME_LOCATION = '/usr/bin/google-chrome'
91    CLIENT_SECRETS = os.path.join(
92        os.path.dirname(__file__), 'client_secrets.json')
93    CLIENT_STORAGE = os.path.join(os.path.dirname(__file__), 'credentials')
94    DEFAULT_CHUNK_SIZE = 1024
95    DOWNLOAD_URL_KEY = '1'
96    EMAIL = 'email'
97    EXPIRED_XSRF_CODE = -32001
98    GETBUILD_ARTIFACTS_KEY = '2'
99    GMS_DOWNLOAD_URL = 'https://partnerdash.google.com/build/gmsdownload'
100    LISTBUILD_BUILD_KEY = '1'
101    PAB_URL = ('https://www.google.com/accounts/Login?&continue='
102               'https://partner.android.com/build/')
103    PASSWORD = 'password'
104    # need both of these scopes to access PAB downloader
105    scopes = ('https://www.googleapis.com/auth/partnerdash',
106              'https://www.googleapis.com/auth/alkali-base')
107    SCOPE = ' '.join(scopes)
108    SVC_URL = urlparse.urljoin(BASE_URL, 'build/u/0/_gwt/_rpc/buildsvc')
109    XSRF_STORE = os.path.join(os.path.dirname(__file__), 'xsrf')
110
111    def __init__(self):
112        """Creates a temp dir."""
113        super(BuildProviderPAB, self).__init__()
114
115    def Authenticate(self, userinfo_file=None, noauth_local_webserver=False,
116                     scopes=SCOPE):
117        """Authenticate using OAuth2.
118
119        Args:
120            userinfo_file: (optional) the path of a JSON file which has
121                           "email" and "password" string fields.
122            noauth_local_webserver: boolean, True if do not (or can not) use
123                                    a local web server.
124            scopes: string or iterable of strings, the scopes to request.
125        """
126        # this should be a JSON file with "email" and "password" string fields
127        self._userinfo_file = userinfo_file
128        logging.info('Parsing flags, use --noauth_local_webserver'
129                     ' if running on remote machine')
130
131        parser = argparse.ArgumentParser(parents=[argparser])
132        flags, unknown = parser.parse_known_args()
133        flags.noauth_local_webserver = noauth_local_webserver
134        logging.info('Preparing OAuth token')
135        flow = flow_from_clientsecrets(self.CLIENT_SECRETS, scope=scopes)
136        storage = Storage(self.CLIENT_STORAGE)
137        if self._credentials is None:
138            self._credentials = storage.get()
139        if self._credentials is None or self._credentials.invalid:
140            logging.info('Credentials not found, authenticating.')
141            self._credentials = run_flow(flow, storage, flags)
142
143        if self._credentials.access_token_expired:
144            logging.info('Access token expired, refreshing.')
145            self._credentials.refresh(http=httplib2.Http())
146
147        if self.XSRF_STORE is not None and os.path.isfile(self.XSRF_STORE):
148            with open(self.XSRF_STORE, 'r') as handle:
149                self._xsrf = handle.read()
150
151    def GetXSRFToken(self, email=None, password=None):
152        """Get XSRF token. Prompt if email/password not provided.
153
154        Args:
155            email: string, optional. Gmail account of user logging in
156            password: string, optional. Password of user logging in
157
158        Returns:
159            boolean, whether the token was accessed and stored
160
161        Raises:
162            ValueError if login fails or userinfo file is malformed.
163        """
164        if self._userinfo_file is not None:
165            with open(self._userinfo_file, 'r') as handle:
166                userinfo = json.load(handle)
167
168            if self.EMAIL not in userinfo or self.PASSWORD not in userinfo:
169                raise ValueError(
170                    'Malformed userinfo file: needs email and password')
171
172            email = userinfo[self.EMAIL]
173            password = userinfo[self.PASSWORD]
174
175        chrome_options = Options()
176        chrome_options.add_argument("--headless")
177
178        driver = webdriver.Chrome(
179            chrome_options=chrome_options)
180
181        driver.set_window_size(1080, 800)
182        wait = WebDriverWait(driver, 10)
183
184        driver.get(self.PAB_URL)
185
186        query = driver.find_element_by_id("identifierId")
187        if email is None:
188            email = raw_input("Email: ")
189        query.send_keys(email)
190        driver.find_element_by_id("identifierNext").click()
191
192        pw = wait.until(EC.element_to_be_clickable((By.NAME, "password")))
193        pw.clear()
194
195        if password is None:
196            pw.send_keys(getpass.getpass("Password: "))
197        else:
198            pw.send_keys(password)
199
200        driver.find_element_by_id("passwordNext").click()
201
202        try:
203            wait.until(EC.title_contains("Partner Android Build"))
204        except TimeoutException as e:
205            logging.exception(e)
206            raise ValueError('Wrong password or non-standard login flow')
207
208        self._xsrf = driver.execute_script("return clientConfig.XSRF_TOKEN;")
209        with open(self.XSRF_STORE, 'w') as handle:
210            handle.write(self._xsrf)
211
212        return True
213
214    def CallBuildsvc(self, method, params, account_id):
215        """Call the buildsvc RPC with given parameters.
216
217        Args:
218            method: string, name of method to be called in buildsvc
219            params: dict, parameters to RPC call
220            account_id: int, ID associated with the PAB account.
221
222        Returns:
223            dict, result from RPC call
224
225        Raises:
226            ValueError if RPC call returns an error or an unknown response.
227        """
228        if self._xsrf is None:
229            self.GetXSRFToken()
230        params = json.dumps(params)
231
232        data = {"method": method, "params": params, "xsrf": self._xsrf}
233        data = json.dumps(data)
234        headers = {}
235        self._credentials.apply(headers)
236        headers['Content-Type'] = 'application/json'
237        headers['x-alkali-account'] = account_id
238
239        try:
240            response = requests.post(self.SVC_URL, data=data, headers=headers,
241                                     timeout=REQUESTS_TIMEOUT_SECONDS)
242        except requests.exceptions.Timeout as e:
243            logging.exception(e)
244            raise ValueError("Request timeout.")
245
246        responseJSON = {}
247
248        try:
249            responseJSON = response.json()
250        except ValueError:
251            raise ValueError("Backend error -- check your account ID")
252
253        if 'result' in responseJSON:
254            return responseJSON['result']
255
256        if 'error' in responseJSON and 'code' in responseJSON['error']:
257            if responseJSON['error']['code'] == self.BAD_XSRF_CODE:
258                raise ValueError(
259                    "Bad XSRF token -- must be for the same account as your credentials")
260            if responseJSON['error']['code'] == self.EXPIRED_XSRF_CODE:
261                raise ValueError("Expired XSRF token -- please refresh")
262
263        raise ValueError("Unknown response from server -- %s" %
264                         json.dumps(responseJSON))
265
266    def GetBuildList(self,
267                     account_id,
268                     branch,
269                     target,
270                     page_token="",
271                     max_results=10,
272                     internal=True,
273                     method=GET,
274                     verify_signed=False):
275        """Get the list of builds for a given account, branch and target
276        Args:
277            account_id: int, ID associated with the PAB account.
278            branch: string, branch to pull resource from.
279            target: string, "latest" or a specific version.
280            page_token: string, token used for pagination
281            max_results: maximum build results the build list contains, e.g. 25
282            internal: bool, whether to query internal build
283            method: 'GET' or 'POST', which endpoint to query
284            verify_signed: bool, whether to verify signed build.
285
286        Returns:
287            list of dicts representing the builds, descending in time
288
289        Raises:
290            ValueError if build request returns an error or builds not found.
291        """
292        if method == POST:
293            params = {
294                "1": branch,
295                "2": target,
296                "3": page_token,
297                "4": max_results,
298                "7": int(internal)
299            }
300
301            result = self.CallBuildsvc("listBuild", params, account_id)
302            # in listBuild response, index '1' contains builds
303            if self.LISTBUILD_BUILD_KEY in result:
304                return result[self.LISTBUILD_BUILD_KEY]
305
306            if verify_signed:
307                logging.error("verify_signed does not support POST method.")
308
309            raise ValueError("Build list not found -- %s" % params)
310        elif method == GET:
311            headers = {}
312            self._credentials.apply(headers)
313
314            action = 'list-internal' if internal else 'list'
315            # PAB URL format expects something (anything) to be given as buildid
316            # and resource, even for action list
317            dummy = 'DUMMY'
318            url = path_urljoin(self.BASE_URL, 'build', 'builds', action,
319                               branch, target, dummy,
320                               dummy) + '?a=' + str(account_id)
321            try:
322                response = requests.get(url, headers=headers,
323                                        timeout=REQUESTS_TIMEOUT_SECONDS)
324                responseJSON = response.json()
325                builds = responseJSON['build']
326            except requests.exceptions.Timeout as e:
327                logging.exception(e)
328                raise ValueError("Request timeout.")
329            except ValueError as e:
330                logging.exception(e)
331                raise ValueError("Backend error -- check your account ID")
332
333            if verify_signed:
334                for build in builds:
335                    artifact_name = "signed%2Fsigned-{}-img-{}.zip".format(
336                        target.split("-")[0], build["build_id"])
337                    logging.debug("Checking whether the build is signed for "
338                                  "build_target {} and build_id {}".format(
339                                      target, build["build_id"]))
340                    signed_build_url = self.GetArtifactURL(
341                        account_id=account_id,
342                        build_id=build["build_id"],
343                        target=target,
344                        artifact_name=artifact_name,
345                        branch=branch,
346                        internal=False,
347                        method=method)
348                    try:
349                        self.GetResponseWithURL(signed_build_url)
350                        logging.debug("The build is signed.")
351                        build["signed"] = True
352                    except requests.HTTPError:
353                        logging.debug("The build is not signed.")
354                        build["signed"] = False
355                    except requests.exceptions.Timeout as e:
356                        logging.debug("Server is not responding.")
357                        logging.exception(e)
358                        build["signed"] = False
359            return builds
360
361    def GetLatestBuildId(self, account_id, branch, target, method=GET):
362        """Get the most recent build_id for a given account, branch and target
363        Args:
364            account_id: int, ID associated with the PAB account.
365            branch: string, branch to pull resource from.
366            target: string, "latest" or a specific version.
367            method: 'GET' or 'POST', which endpoint to query
368
369        Returns:
370            string, most recent build id
371
372        Raises:
373            ValueError if complete builds are not found.
374        """
375        # TODO: support pagination, maybe?
376        build_list = self.GetBuildList(account_id=account_id,
377                                       branch=branch,
378                                       target=target,
379                                       method=method)
380        if len(build_list) == 0:
381            raise ValueError(
382                'No builds found for account_id=%s, branch=%s, target=%s' %
383                (account_id, branch, target))
384        for build in build_list:
385            if method == POST:
386                # get build status: 7 = completed build
387                if build.get(self.BUILD_STATUS_KEY,
388                             None) == self.BUILD_COMPLETED_STATUS:
389                    # return build id (index '1')
390                    return build[self.BUILD_BUILDID_KEY]
391            elif method == GET:
392                if build['build_attempt_status'] == "COMPLETE" and build[
393                        "successful"]:
394                    return build['build_id']
395        raise ValueError(
396            'No complete builds found: %s failed or incomplete builds found' %
397            len(build_list))
398
399    def GetBuildArtifacts(
400            self, account_id, build_id, branch, target, method=POST):
401        """Get the list of build artifacts.
402
403        For an account, build, target, branch.
404
405        Args:
406            account_id: int, ID associated with the PAB account.
407            build_id: string, ID of the build
408            branch: string, branch to pull resource from.
409            target: string, "latest" or a specific version.
410            method: 'GET' or 'POST', which endpoint to query
411
412        Returns:
413            list of build artifact objects
414
415        Raises:
416            NotImplementedError if method is 'GET', which is not supported yet.
417            ValueError if build artifacts are not found.
418        """
419        if method == GET:
420            raise NotImplementedError(
421                "GetBuildArtifacts not supported with GET")
422        params = {"1": build_id, "2": target, "3": branch}
423
424        result = self.CallBuildsvc("getBuild", params, account_id)
425        # in getBuild response, index '2' contains the artifacts
426        if self.GETBUILD_ARTIFACTS_KEY in result:
427            return result[self.GETBUILD_ARTIFACTS_KEY]
428        if len(result) == 0:
429            raise ValueError("Build artifacts not found -- %s" % params)
430
431    def GetArtifactURL(self,
432                       account_id,
433                       build_id,
434                       target,
435                       artifact_name,
436                       branch,
437                       internal,
438                       method=GET):
439        """Get the URL for an artifact on the PAB server, using buildsvc.
440
441        Args:
442            account_id: int, ID associated with the PAB account.
443            build_id: string/int, id of the build.
444            target: string, "latest" or a specific version.
445            artifact_name: string, simple file name (no parent dir or path).
446            branch: string, branch to pull resource from.
447            internal: int, whether the request is for an internal build artifact
448            method: 'GET' or 'POST', which endpoint to query
449
450        Returns:
451            string, The URL for the resource specified by the parameters
452
453        Raises:
454            ValueError if given parameters are incorrect or resource not found.
455        """
456        if method == POST:
457            params = {
458                "1": str(build_id),
459                "2": target,
460                "3": artifact_name,
461                "4": branch,
462                "5": "",  # release_candidate_name
463                "6": internal
464            }
465
466            result = self.CallBuildsvc(method='downloadBuildArtifact',
467                                       params=params,
468                                       account_id=account_id)
469
470            # in downloadBuildArtifact response, index '1' contains the url
471            if self.DOWNLOAD_URL_KEY in result:
472                return result[self.DOWNLOAD_URL_KEY]
473            if len(result) == 0:
474                raise ValueError("Resource not found -- %s" % params)
475        elif method == GET:
476            headers = {}
477            self._credentials.apply(headers)
478
479            action = 'get-internal' if internal else 'get'
480            get_url = path_urljoin(self.BASE_URL, 'build', 'builds', action,
481                                   branch, target, build_id,
482                                   artifact_name) + '?a=' + str(account_id)
483
484            try:
485                response = requests.get(get_url, headers=headers,
486                                        timeout=REQUESTS_TIMEOUT_SECONDS)
487                responseJSON = response.json()
488                return responseJSON['url']
489            except requests.exceptions.Timeout as e:
490                logging.exception(e)
491                raise ValueError("Request timeout.")
492            except ValueError:
493                raise ValueError("Backend error -- check your account ID")
494
495    def DownloadArtifact(self, download_url, filename):
496        """Get artifact from Partner Android Build server.
497
498        Args:
499            download_url: location of resource that we want to download
500            filename: where the artifact gets downloaded locally.
501
502        Returns:
503            boolean, whether the file was successfully downloaded
504        """
505        try:
506            response = self.GetResponseWithURL(download_url)
507        except (requests.HTTPError, requests.exceptions.Timeout) as error:
508            logging.exception(error)
509            return False
510        logging.info('%s now downloading...', download_url)
511        with open(filename, 'wb') as handle:
512            for block in response.iter_content(self.DEFAULT_CHUNK_SIZE):
513                handle.write(block)
514        return True
515
516    def GetArtifact(self,
517                    account_id,
518                    branch,
519                    target,
520                    artifact_name,
521                    build_id='latest',
522                    method=GET,
523                    full_device_images=False):
524        """Get an artifact for an account, branch, target and name and build id.
525
526        If build_id not given, get latest.
527
528        Args:
529            account_id: int, ID associated with the PAB account.
530            branch: string, branch to pull resource from.
531            target: string, "latest" or a specific version.
532            artifact_name: name of artifact, e.g. aosp_arm64_ab-img-4353141.zip
533                ({id} will automatically get replaced with build ID)
534            build_id: string, build ID of an artifact to fetch (or 'latest').
535            method: 'GET' or 'POST', which endpoint to query.
536
537        Returns:
538            a dict containing the device image info.
539            a dict containing the test suite package info.
540            a dict containing the artifact info.
541            a dict containing the global config info.
542
543        Raises:
544            ValueError if artifacts are not found.
545        """
546        artifact_info = {}
547        if build_id == 'latest':
548            build_id = self.GetLatestBuildId(account_id=account_id,
549                                             branch=branch,
550                                             target=target,
551                                             method=method)
552            logging.info("latest build ID = %s", build_id)
553        artifact_info["build_id"] = build_id
554
555        if "build_id" in artifact_name:
556            artifact_name = artifact_name.format(build_id=build_id)
557
558        if method == POST:
559            artifacts = self.GetBuildArtifacts(account_id=account_id,
560                                               build_id=build_id,
561                                               branch=branch,
562                                               target=target,
563                                               method=method)
564
565            if len(artifacts) == 0:
566                raise ValueError(
567                    "No artifacts found for build_id=%s, branch=%s, target=%s"
568                    % (build_id, branch, target))
569
570            # in build artifact response, index '1' contains the name
571            artifact_names = [
572                artifact[self.BUILDARTIFACT_NAME_KEY] for artifact in artifacts
573            ]
574            if artifact_name not in artifact_names:
575                raise ValueError("%s not found in artifact list" %
576                                 artifact_name)
577
578        url = self.GetArtifactURL(account_id=account_id,
579                                  build_id=build_id,
580                                  target=target,
581                                  artifact_name=artifact_name,
582                                  branch=branch,
583                                  internal=False,
584                                  method=method)
585
586        if self.tmp_dirpath:
587            artifact_path = os.path.join(self.tmp_dirpath, artifact_name)
588        else:
589            artifact_path = artifact_name
590        self.DownloadArtifact(url, artifact_path)
591
592        self.SetFetchedFile(
593            artifact_path, full_device_images=full_device_images)
594
595        return (self.GetDeviceImage(), self.GetTestSuitePackage(),
596                artifact_info, self.GetConfigPackage())
597
598    def GetSignedBuildArtifact(self,
599                               account_id,
600                               branch,
601                               target,
602                               artifact_name,
603                               build_id='latest',
604                               method=GET,
605                               full_device_images=False):
606        """Get an signed build artifact from the PAB bulid list.
607
608        Args:
609            account_id: int, ID associated with the PAB account.
610            branch: string, branch to pull resource from.
611            target: string, "latest" or a specific version.
612            artifact_name: name of artifact, e.g. aosp_arm64_ab-img-4353141.zip
613                ({id} will automatically get replaced with build ID)
614            build_id: string, build ID of an artifact to fetch (or 'latest').
615            method: 'GET' or 'POST', which endpoint to query.
616
617        Returns:
618            a dict containing the device image info.
619            a dict containing the test suite package info.
620            a dict containing the artifact info.
621            a dict containing the global config info.
622        """
623        artifact_info = {}
624        build_ids = []
625        artifact_path = ""
626        if build_id == 'latest':
627            try:
628                build_list = self.GetBuildList(
629                    account_id=account_id,
630                    branch=branch,
631                    target=target,
632                    method=method)
633                for build in build_list:
634                    build_ids.append(build["build_id"])
635            except ValueError as e:
636                logging.exception(e)
637        else:
638            build_ids.append(build_id)
639
640        for build_id in build_ids:
641            _artifact_name = artifact_name
642            if "build_id" in _artifact_name:
643                _artifact_name = _artifact_name.format(build_id=build_id)
644            _artifact_name = "signed%2Fsigned-" + _artifact_name
645            try:
646                url = self.GetArtifactURL(
647                    account_id=account_id,
648                    build_id=build_id,
649                    target=target,
650                    artifact_name=_artifact_name,
651                    branch=branch,
652                    internal=False,
653                    method=method)
654            except ValueError as e:
655                logging.exception(e)
656                continue
657
658            if self.tmp_dirpath:
659                artifact_path = os.path.join(self.tmp_dirpath, _artifact_name)
660            else:
661                artifact_path = _artifact_name
662            ret = self.DownloadArtifact(url, artifact_path)
663
664            if ret:
665                artifact_info["build_id"] = build_id
666                break
667
668        self.SetFetchedFile(
669            artifact_path, full_device_images=full_device_images)
670
671        return (self.GetDeviceImage(), self.GetTestSuitePackage(),
672                artifact_info, self.GetConfigPackage())
673
674    def GetResponseWithURL(self, url):
675        """Gets the response content from the server connected with the url.
676
677        Args:
678            url: A string representing the server url.
679
680        Returns:
681            A Response object received from the server.
682
683        Raises:
684            requests.HTTPError if response.status_code is not 200.
685            requests.exceptions.Timeout if the server does not respond.
686        """
687        headers = {}
688        self._credentials.apply(headers)
689        response = requests.get(url, headers=headers, stream=True,
690                                timeout=REQUESTS_TIMEOUT_SECONDS)
691        response.raise_for_status()
692
693        return response
694
695    def FetchLatestBuiltHCPackage(self, account_id, branch, target):
696        """Fetchs the latest <artifact_name> file and return the path.
697
698        Args:
699            account_id: string, Partner Android Build account_id to use.
700            branch: string, branch to grab the artifact from.
701            targets: string, a comma-separate list of build target product(s).
702
703        Returns:
704            path to the fetched file. None if the fetching has failed.
705        """
706        try:
707            listed_builds = self.GetBuildList(
708                account_id=account_id,
709                branch=branch,
710                target=target,
711                page_token="",
712                max_results=1,
713                method="GET")
714
715            if listed_builds and len(listed_builds) > 0:
716                for listed_build in listed_builds:
717                    if listed_build["successful"]:
718                        self.GetArtifact(
719                            account_id=account_id,
720                            branch=branch,
721                            target=target,
722                            artifact_name="android-vtslab.zip",
723                            build_id=listed_build["build_id"],
724                            method="GET")
725
726                        return self.GetHostControllerPackage("vtslab")
727        except ValueError as e:
728            logging.exception(e)
729