1*6dbdd20aSAndroid Build Coastguard Worker# Copyright (C) 2019 The Android Open Source Project 2*6dbdd20aSAndroid Build Coastguard Worker# 3*6dbdd20aSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 4*6dbdd20aSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 5*6dbdd20aSAndroid Build Coastguard Worker# You may obtain a copy of the License at 6*6dbdd20aSAndroid Build Coastguard Worker# 7*6dbdd20aSAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 8*6dbdd20aSAndroid Build Coastguard Worker# 9*6dbdd20aSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 10*6dbdd20aSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 11*6dbdd20aSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*6dbdd20aSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 13*6dbdd20aSAndroid Build Coastguard Worker# limitations under the License. 14*6dbdd20aSAndroid Build Coastguard Worker 15*6dbdd20aSAndroid Build Coastguard Workerimport asyncio 16*6dbdd20aSAndroid Build Coastguard Workerimport concurrent.futures 17*6dbdd20aSAndroid Build Coastguard Workerimport google.auth 18*6dbdd20aSAndroid Build Coastguard Workerimport google.auth.transport.requests 19*6dbdd20aSAndroid Build Coastguard Workerimport json 20*6dbdd20aSAndroid Build Coastguard Workerimport logging 21*6dbdd20aSAndroid Build Coastguard Workerimport os 22*6dbdd20aSAndroid Build Coastguard Workerimport requests 23*6dbdd20aSAndroid Build Coastguard Worker 24*6dbdd20aSAndroid Build Coastguard Workerfrom base64 import b64encode 25*6dbdd20aSAndroid Build Coastguard Workerfrom datetime import datetime 26*6dbdd20aSAndroid Build Coastguard Workerfrom config import PROJECT 27*6dbdd20aSAndroid Build Coastguard Worker 28*6dbdd20aSAndroid Build Coastguard Worker# Thread pool for making http requests asynchronosly. 29*6dbdd20aSAndroid Build Coastguard Workerthread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=8) 30*6dbdd20aSAndroid Build Coastguard Worker 31*6dbdd20aSAndroid Build Coastguard Worker# Caller has to initialize this 32*6dbdd20aSAndroid Build Coastguard WorkerSCOPES = [] 33*6dbdd20aSAndroid Build Coastguard Workercached_gerrit_creds = None 34*6dbdd20aSAndroid Build Coastguard Workercached_oauth2_creds = None 35*6dbdd20aSAndroid Build Coastguard Worker 36*6dbdd20aSAndroid Build Coastguard Worker 37*6dbdd20aSAndroid Build Coastguard Workerclass ConcurrentModificationError(Exception): 38*6dbdd20aSAndroid Build Coastguard Worker pass 39*6dbdd20aSAndroid Build Coastguard Worker 40*6dbdd20aSAndroid Build Coastguard Worker 41*6dbdd20aSAndroid Build Coastguard Workerdef get_access_token(): 42*6dbdd20aSAndroid Build Coastguard Worker global cached_oauth2_creds 43*6dbdd20aSAndroid Build Coastguard Worker creds = cached_oauth2_creds 44*6dbdd20aSAndroid Build Coastguard Worker if creds is None or not creds.valid or creds.expired: 45*6dbdd20aSAndroid Build Coastguard Worker creds, _project = google.auth.default(scopes=SCOPES) 46*6dbdd20aSAndroid Build Coastguard Worker request = google.auth.transport.requests.Request() 47*6dbdd20aSAndroid Build Coastguard Worker creds.refresh(request) 48*6dbdd20aSAndroid Build Coastguard Worker cached_oauth2_creds = creds 49*6dbdd20aSAndroid Build Coastguard Worker return creds.token 50*6dbdd20aSAndroid Build Coastguard Worker 51*6dbdd20aSAndroid Build Coastguard Worker 52*6dbdd20aSAndroid Build Coastguard Workerdef get_gerrit_credentials(): 53*6dbdd20aSAndroid Build Coastguard Worker '''Retrieve the credentials used to authenticate Gerrit requests 54*6dbdd20aSAndroid Build Coastguard Worker 55*6dbdd20aSAndroid Build Coastguard Worker Returns a tuple (user, gitcookie). These fields are obtained from the Gerrit 56*6dbdd20aSAndroid Build Coastguard Worker 'New HTTP password' page which generates a .gitcookie file and stored in the 57*6dbdd20aSAndroid Build Coastguard Worker project datastore. 58*6dbdd20aSAndroid Build Coastguard Worker user: typically looks like git-user.gmail.com. 59*6dbdd20aSAndroid Build Coastguard Worker gitcookie: is the password after the = token. 60*6dbdd20aSAndroid Build Coastguard Worker ''' 61*6dbdd20aSAndroid Build Coastguard Worker global cached_gerrit_creds 62*6dbdd20aSAndroid Build Coastguard Worker if cached_gerrit_creds is None: 63*6dbdd20aSAndroid Build Coastguard Worker body = {'query': {'kind': [{'name': 'GerritAuth'}]}} 64*6dbdd20aSAndroid Build Coastguard Worker res = req( 65*6dbdd20aSAndroid Build Coastguard Worker 'POST', 66*6dbdd20aSAndroid Build Coastguard Worker 'https://datastore.googleapis.com/v1/projects/%s:runQuery' % PROJECT, 67*6dbdd20aSAndroid Build Coastguard Worker body=body) 68*6dbdd20aSAndroid Build Coastguard Worker auth = res['batch']['entityResults'][0]['entity']['properties'] 69*6dbdd20aSAndroid Build Coastguard Worker user = auth['user']['stringValue'] 70*6dbdd20aSAndroid Build Coastguard Worker gitcookie = auth['gitcookie']['stringValue'] 71*6dbdd20aSAndroid Build Coastguard Worker cached_gerrit_creds = user, gitcookie 72*6dbdd20aSAndroid Build Coastguard Worker return cached_gerrit_creds 73*6dbdd20aSAndroid Build Coastguard Worker 74*6dbdd20aSAndroid Build Coastguard Worker 75*6dbdd20aSAndroid Build Coastguard Workerasync def req_async(method, url, body=None, gerrit=False): 76*6dbdd20aSAndroid Build Coastguard Worker loop = asyncio.get_running_loop() 77*6dbdd20aSAndroid Build Coastguard Worker # run_in_executor cannot take kwargs, we need to stick with order. 78*6dbdd20aSAndroid Build Coastguard Worker return await loop.run_in_executor(thread_pool, req, method, url, body, gerrit, 79*6dbdd20aSAndroid Build Coastguard Worker False, None) 80*6dbdd20aSAndroid Build Coastguard Worker 81*6dbdd20aSAndroid Build Coastguard Worker 82*6dbdd20aSAndroid Build Coastguard Workerdef req(method, url, body=None, gerrit=False, req_etag=False, etag=None): 83*6dbdd20aSAndroid Build Coastguard Worker '''Helper function to handle authenticated HTTP requests. 84*6dbdd20aSAndroid Build Coastguard Worker 85*6dbdd20aSAndroid Build Coastguard Worker Cloud API and Gerrit require two different types of authentication and as 86*6dbdd20aSAndroid Build Coastguard Worker such need to be handled differently. The HTTP connection is cached in the 87*6dbdd20aSAndroid Build Coastguard Worker TLS slot to avoid refreshing oauth tokens too often for back-to-back requests. 88*6dbdd20aSAndroid Build Coastguard Worker Appengine takes care of clearing the TLS slot upon each frontend request so 89*6dbdd20aSAndroid Build Coastguard Worker these connections won't be recycled for too long. 90*6dbdd20aSAndroid Build Coastguard Worker ''' 91*6dbdd20aSAndroid Build Coastguard Worker hdr = {'Content-Type': 'application/json; charset=UTF-8'} 92*6dbdd20aSAndroid Build Coastguard Worker if gerrit: 93*6dbdd20aSAndroid Build Coastguard Worker creds = '%s:%s' % get_gerrit_credentials() 94*6dbdd20aSAndroid Build Coastguard Worker auth_header = 'Basic ' + b64encode(creds.encode('utf-8')).decode('utf-8') 95*6dbdd20aSAndroid Build Coastguard Worker elif SCOPES: 96*6dbdd20aSAndroid Build Coastguard Worker auth_header = 'Bearer ' + get_access_token() 97*6dbdd20aSAndroid Build Coastguard Worker logging.debug('%s %s [gerrit=%d]', method, url, gerrit) 98*6dbdd20aSAndroid Build Coastguard Worker hdr['Authorization'] = auth_header 99*6dbdd20aSAndroid Build Coastguard Worker if req_etag: 100*6dbdd20aSAndroid Build Coastguard Worker hdr['X-Firebase-ETag'] = 'true' 101*6dbdd20aSAndroid Build Coastguard Worker if etag: 102*6dbdd20aSAndroid Build Coastguard Worker hdr['if-match'] = etag 103*6dbdd20aSAndroid Build Coastguard Worker body = None if body is None else json.dumps(body) 104*6dbdd20aSAndroid Build Coastguard Worker resp = requests.request(method, url, headers=hdr, data=body, timeout=60) 105*6dbdd20aSAndroid Build Coastguard Worker res = resp.content.decode('utf-8') 106*6dbdd20aSAndroid Build Coastguard Worker resp_etag = resp.headers.get('etag') 107*6dbdd20aSAndroid Build Coastguard Worker if resp.status_code == 200: 108*6dbdd20aSAndroid Build Coastguard Worker # [4:] is to strip Gerrit XSSI projection prefix. 109*6dbdd20aSAndroid Build Coastguard Worker res = json.loads(res[4:] if gerrit else res) 110*6dbdd20aSAndroid Build Coastguard Worker return (res, resp_etag) if req_etag else res 111*6dbdd20aSAndroid Build Coastguard Worker elif resp.status_code == 412: 112*6dbdd20aSAndroid Build Coastguard Worker raise ConcurrentModificationError() 113*6dbdd20aSAndroid Build Coastguard Worker else: 114*6dbdd20aSAndroid Build Coastguard Worker raise Exception(resp, res) 115*6dbdd20aSAndroid Build Coastguard Worker 116*6dbdd20aSAndroid Build Coastguard Worker 117*6dbdd20aSAndroid Build Coastguard Worker# Datetime functions to deal with the fact that Javascript expects a trailing 118*6dbdd20aSAndroid Build Coastguard Worker# 'Z' (Z == 'Zulu' == UTC) for timestamps. 119*6dbdd20aSAndroid Build Coastguard Workerdef parse_iso_time(time_str): 120*6dbdd20aSAndroid Build Coastguard Worker return datetime.strptime(time_str, r'%Y-%m-%dT%H:%M:%SZ') 121*6dbdd20aSAndroid Build Coastguard Worker 122*6dbdd20aSAndroid Build Coastguard Worker 123*6dbdd20aSAndroid Build Coastguard Workerdef utc_now_iso(utcnow=None): 124*6dbdd20aSAndroid Build Coastguard Worker return (utcnow or datetime.utcnow()).strftime(r'%Y-%m-%dT%H:%M:%SZ') 125*6dbdd20aSAndroid Build Coastguard Worker 126*6dbdd20aSAndroid Build Coastguard Worker 127*6dbdd20aSAndroid Build Coastguard Workerdef defer(coro): 128*6dbdd20aSAndroid Build Coastguard Worker loop = asyncio.get_event_loop() 129*6dbdd20aSAndroid Build Coastguard Worker task = loop.create_task(coro) 130*6dbdd20aSAndroid Build Coastguard Worker task.set_name(coro.cr_code.co_name) 131*6dbdd20aSAndroid Build Coastguard Worker return task 132*6dbdd20aSAndroid Build Coastguard Worker 133*6dbdd20aSAndroid Build Coastguard Worker 134*6dbdd20aSAndroid Build Coastguard Workerdef init_logging(): 135*6dbdd20aSAndroid Build Coastguard Worker logging.basicConfig( 136*6dbdd20aSAndroid Build Coastguard Worker format='%(levelname)-8s %(asctime)s %(message)s', 137*6dbdd20aSAndroid Build Coastguard Worker level=logging.DEBUG if os.getenv('VERBOSE') else logging.INFO, 138*6dbdd20aSAndroid Build Coastguard Worker datefmt=r'%Y-%m-%d %H:%M:%S') 139