| # Copyright (C) 2019 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. |
| |
| import json |
| import httplib2 |
| import os |
| import logging |
| import threading |
| |
| from datetime import datetime |
| from oauth2client.client import GoogleCredentials |
| from config import PROJECT |
| |
| tls = threading.local() |
| |
| # Caller has to initialize this |
| SCOPES = [] |
| |
| |
| class ConcurrentModificationError(Exception): |
| pass |
| |
| |
| def get_gerrit_credentials(): |
| '''Retrieve the credentials used to authenticate Gerrit requests |
| |
| Returns a tuple (user, gitcookie). These fields are obtained from the Gerrit |
| 'New HTTP password' page which generates a .gitcookie file and stored in the |
| project datastore. |
| user: typically looks like git-user.gmail.com. |
| gitcookie: is the password after the = token. |
| ''' |
| body = {'query': {'kind': [{'name': 'GerritAuth'}]}} |
| res = req( |
| 'POST', |
| 'https://datastore.googleapis.com/v1/projects/%s:runQuery' % PROJECT, |
| body=body) |
| auth = res['batch']['entityResults'][0]['entity']['properties'] |
| user = auth['user']['stringValue'] |
| gitcookie = auth['gitcookie']['stringValue'] |
| return user, gitcookie |
| |
| |
| def req(method, uri, body=None, req_etag=False, etag=None, gerrit=False): |
| '''Helper function to handle authenticated HTTP requests. |
| |
| Cloud API and Gerrit require two different types of authentication and as |
| such need to be handled differently. The HTTP connection is cached in the |
| TLS slot to avoid refreshing oauth tokens too often for back-to-back requests. |
| Appengine takes care of clearing the TLS slot upon each frontend request so |
| these connections won't be recycled for too long. |
| ''' |
| hdr = {'Content-Type': 'application/json; charset=UTF-8'} |
| tls_key = 'gerrit_http' if gerrit else 'oauth2_http' |
| if hasattr(tls, tls_key): |
| http = getattr(tls, tls_key) |
| else: |
| http = httplib2.Http() |
| setattr(tls, tls_key, http) |
| if gerrit: |
| http.add_credentials(*get_gerrit_credentials()) |
| elif SCOPES: |
| creds = GoogleCredentials.get_application_default().create_scoped(SCOPES) |
| creds.authorize(http) |
| |
| if req_etag: |
| hdr['X-Firebase-ETag'] = 'true' |
| if etag: |
| hdr['if-match'] = etag |
| body = None if body is None else json.dumps(body) |
| logging.debug('%s %s', method, uri) |
| resp, res = http.request(uri, method=method, headers=hdr, body=body) |
| if resp.status == 200: |
| res = res[4:] if gerrit else res # Strip Gerrit XSSI projection chars. |
| return (json.loads(res), resp['etag']) if req_etag else json.loads(res) |
| elif resp.status == 412: |
| raise ConcurrentModificationError() |
| else: |
| delattr(tls, tls_key) |
| raise Exception(resp, res) |
| |
| |
| # Datetime functions to deal with the fact that Javascript expects a trailing |
| # 'Z' (Z == 'Zulu' == UTC) for timestamps. |
| |
| |
| def parse_iso_time(time_str): |
| return datetime.strptime(time_str, r'%Y-%m-%dT%H:%M:%SZ') |
| |
| |
| def utc_now_iso(utcnow=None): |
| return (utcnow or datetime.utcnow()).strftime(r'%Y-%m-%dT%H:%M:%SZ') |
| |
| |
| def init_logging(): |
| logging.basicConfig( |
| format='%(asctime)s %(levelname)-8s %(message)s', |
| level=logging.DEBUG if os.getenv('VERBOSE') else logging.INFO, |
| datefmt=r'%Y-%m-%d %H:%M:%S') |