blob: 2d168e9aff1a911466a87433ac38bc1a52058455 [file] [log] [blame]
xixuan878b1eb2017-03-20 15:58:17 -07001# Copyright 2017 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Module for interacting with google APIs."""
Xixuan Wu5d6063e2017-09-05 16:15:07 -07006# pylint: disable=g-bad-import-order
Xixuan Wud55ac6e2019-03-14 10:56:39 -07007# pylint: disable=g-bad-exception-name
Xixuan Wu5d6063e2017-09-05 16:15:07 -07008
9import httplib2
Xixuan Wu55d38c52019-05-21 14:26:23 -070010import logging
xixuan878b1eb2017-03-20 15:58:17 -070011
12import apiclient
xixuan878b1eb2017-03-20 15:58:17 -070013import constants
14import file_getter
15
16from oauth2client import service_account
17from oauth2client.contrib import appengine
18
19
20class RestClientError(Exception):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070021 """Raised when there is a general error."""
xixuan878b1eb2017-03-20 15:58:17 -070022
23
24class NoServiceRestClientError(RestClientError):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070025 """Raised when there is no ready service for a google API."""
xixuan878b1eb2017-03-20 15:58:17 -070026
27
28class BaseRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070029 """Base class of REST client for google APIs."""
xixuan878b1eb2017-03-20 15:58:17 -070030
Xixuan Wu5d6063e2017-09-05 16:15:07 -070031 def __init__(self, scopes, service_name, service_version):
32 """Initialize a REST client to connect to a google API.
xixuan878b1eb2017-03-20 15:58:17 -070033
Xixuan Wu5d6063e2017-09-05 16:15:07 -070034 Args:
35 scopes: the scopes of the to-be-connected API.
36 service_name: the service name of the to-be-connected API.
37 service_version: the service version of the to-be-connected API.
38 """
39 self.running_env = constants.environment()
40 self.scopes = scopes
41 self.service_name = service_name
42 self.service_version = service_version
xixuan878b1eb2017-03-20 15:58:17 -070043
Xixuan Wu5d6063e2017-09-05 16:15:07 -070044 @property
45 def service(self):
46 if not self._service:
47 raise NoServiceRestClientError('No service created for calling API')
xixuan878b1eb2017-03-20 15:58:17 -070048
Xixuan Wu5d6063e2017-09-05 16:15:07 -070049 return self._service
xixuan878b1eb2017-03-20 15:58:17 -070050
Xixuan Wu5d6063e2017-09-05 16:15:07 -070051 def create_service(self, discovery_url=None):
52 """Create the service for a google API."""
53 self._init_credentials()
54 # Explicitly specify timeout for http to avoid DeadlineExceededError.
55 # It's used for services like AndroidBuild API, which raise such error
56 # when being triggered too many calls in a short time frame.
57 # http://stackoverflow.com/questions/14698119/httpexception-deadline-exceeded-while-waiting-for-http-response-from-url-dead
58 http_auth = self._credentials.authorize(httplib2.Http(timeout=30))
59 if discovery_url is None:
60 self._service = apiclient.discovery.build(
61 self.service_name, self.service_version,
62 http=http_auth)
63 else:
64 self._service = apiclient.discovery.build(
65 self.service_name, self.service_version, http=http_auth,
66 discoveryServiceUrl=discovery_url)
xixuan878b1eb2017-03-20 15:58:17 -070067
Xixuan Wu5d6063e2017-09-05 16:15:07 -070068 def _init_credentials(self):
69 """Initialize the credentials for a google API."""
70 if (self.running_env == constants.RunningEnv.ENV_STANDALONE or
71 self.running_env == constants.RunningEnv.ENV_DEVELOPMENT_SERVER):
72 # Running locally
73 service_credentials = service_account.ServiceAccountCredentials
74 self._credentials = service_credentials.from_json_keyfile_name(
Xixuan Wu26d06e02017-09-20 14:50:28 -070075 file_getter.STAGING_CLIENT_SECRETS_FILE, self.scopes)
Xixuan Wu5d6063e2017-09-05 16:15:07 -070076 else:
77 # Running in app-engine production
78 self._credentials = appengine.AppAssertionCredentials(self.scopes)
xixuan878b1eb2017-03-20 15:58:17 -070079
80
81class AndroidBuildRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070082 """REST client for android build API."""
xixuan878b1eb2017-03-20 15:58:17 -070083
Xixuan Wu5d6063e2017-09-05 16:15:07 -070084 def __init__(self, rest_client):
85 """Initialize a REST client for connecting to Android Build API."""
86 self._rest_client = rest_client
87 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -070088
Xixuan Wu5d6063e2017-09-05 16:15:07 -070089 def get_latest_build_id(self, branch, target):
90 """Get the latest build id for a given branch and target.
xixuan878b1eb2017-03-20 15:58:17 -070091
Xixuan Wu5d6063e2017-09-05 16:15:07 -070092 Args:
93 branch: an android build's branch
94 target: an android build's target
xixuan878b1eb2017-03-20 15:58:17 -070095
Xixuan Wu5d6063e2017-09-05 16:15:07 -070096 Returns:
97 A string representing latest build id.
98 """
99 request = self._rest_client.service.build().list(
100 buildType='submitted',
101 branch=branch,
102 target=target,
103 successful=True,
104 maxResults=1)
105 builds = request.execute(num_retries=10)
106 if not builds or not builds['builds']:
107 return None
xixuan878b1eb2017-03-20 15:58:17 -0700108
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700109 return builds['builds'][0]['buildId']
xixuan878b1eb2017-03-20 15:58:17 -0700110
111
112class StorageRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700113 """REST client for google storage API."""
xixuan878b1eb2017-03-20 15:58:17 -0700114
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700115 def __init__(self, rest_client):
116 """Initialize a REST client for connecting to Google storage API."""
117 self._rest_client = rest_client
118 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -0700119
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700120 def read_object(self, bucket, object_path):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700121 """Read the contents of input_object in input_bucket.
xixuan878b1eb2017-03-20 15:58:17 -0700122
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700123 Args:
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700124 bucket: A string to indicate the bucket for fetching the object.
125 e.g. constants.StorageBucket.PROD_SUITE_SCHEDULER
126 object_path: A string to indicate the path of the object to read the
127 contents.
xixuan878b1eb2017-03-20 15:58:17 -0700128
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700129 Returns:
130 the stripped string contents of the input object.
xixuan878b1eb2017-03-20 15:58:17 -0700131
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700132 Raises:
133 apiclient.errors.HttpError
134 """
135 req = self._rest_client.service.objects().get_media(
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700136 bucket=bucket,
137 object=object_path)
138 return req.execute()
139
140 def upload_object(self, bucket, src_object_path, dest_object_path):
141 """Upload object_path to input_bucket.
142
143 Args:
144 bucket: A string to indicate the bucket for the object to be uploaded to.
145 src_object_path: A string the full path of the object to upload.
146 dest_object_path: A string path inside bucket to upload to.
147
148 Returns:
149 A dict of uploaded object info.
150
151 Raises:
152 apiclient.errors.HttpError
153 """
154 req = self._rest_client.service.objects().insert(
155 bucket=bucket,
156 name=dest_object_path,
157 media_body=src_object_path,
158 media_mime_type='text/plain',
159 )
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700160 return req.execute()
xixuan878b1eb2017-03-20 15:58:17 -0700161
162
163class CalendarRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700164 """Class of REST client for google calendar API."""
xixuan878b1eb2017-03-20 15:58:17 -0700165
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700166 def __init__(self, rest_client):
167 """Initialize a REST client for connecting to Google calendar API."""
168 self._rest_client = rest_client
169 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -0700170
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700171 def add_event(self, calendar_id, input_event):
172 """Add events of a given calendar.
xixuan878b1eb2017-03-20 15:58:17 -0700173
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700174 Args:
175 calendar_id: the ID of the given calendar.
176 input_event: the event to be added.
177 """
178 self._rest_client.service.events().insert(
179 calendarId=calendar_id,
180 body=input_event).execute()
xixuan878b1eb2017-03-20 15:58:17 -0700181
182
Xixuan Wu6f117e92017-10-27 10:51:58 -0700183class StackdriverRestClient(object):
184 """REST client for google storage API."""
185
186 def __init__(self, rest_client):
187 """Initialize a REST client for connecting to Google storage API."""
188 self._rest_client = rest_client
189 self._rest_client.create_service()
190
191 def read_logs(self, request):
192 # project_id, page_size, order_by, query_filter=''):
193 """Read the logs of the project_id based on all filters.
194
195 Args:
196 request: a request dict generated by
197 stackdriver_lib.form_logging_client_request.
198
199 Returns:
200 A json object, can be parsed by
201 stackdriver_lib.parse_logging_client_response.
202
203 Raises:
204 apiclient.errors.HttpError
205 """
206 req = self._rest_client.service.entries().list(
207 fields='entries/protoPayload', body=request)
208 return req.execute()
209
210
xixuan878b1eb2017-03-20 15:58:17 -0700211class SwarmingRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700212 """REST client for swarming proxy API."""
xixuan878b1eb2017-03-20 15:58:17 -0700213
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700214 DISCOVERY_URL_PATTERN = '%s/discovery/v1/apis/%s/%s/rest'
xixuan878b1eb2017-03-20 15:58:17 -0700215
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700216 def __init__(self, rest_client, service_url):
217 self._rest_client = rest_client
218 discovery_url = self.DISCOVERY_URL_PATTERN % (
219 service_url, rest_client.service_name, rest_client.service_version)
220 self._rest_client.create_service(discovery_url=discovery_url)
xixuan878b1eb2017-03-20 15:58:17 -0700221
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700222 def create_task(self, request):
223 """Create new task.
xixuan878b1eb2017-03-20 15:58:17 -0700224
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700225 Args:
226 request: a json-compatible dict expected by swarming server.
227 See _to_raw_request's output in swarming_lib.py for details.
xixuan878b1eb2017-03-20 15:58:17 -0700228
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700229 Returns:
230 A json dict returned by API task.new.
231 """
232 return self._rest_client.service.tasks().new(
233 fields='request,task_id', body=request).execute()
xixuan878b1eb2017-03-20 15:58:17 -0700234
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700235 def get_task_result(self, task_id):
236 """Get task results by a given task_id.
xixuan878b1eb2017-03-20 15:58:17 -0700237
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700238 Args:
239 task_id: A string, represents task id.
240
241 Returns:
242 A json dict returned by API task.result.
243 """
244 return self._rest_client.service.task().result(
245 task_id=task_id).execute()
Xixuan Wu7d142a92019-04-26 12:03:02 -0700246
247
248class BigqueryRestClient(object):
249 """Class of REST client for Bigquery API."""
250
Xixuan Wu55d38c52019-05-21 14:26:23 -0700251 PROJECT_TO_RUN_BIGQUERY_JOB = 'google.com:suite-scheduler'
252
Xixuan Wu7d142a92019-04-26 12:03:02 -0700253 def __init__(self, rest_client):
254 """Initialize a REST client for connecting to Bigquery API."""
255 self._rest_client = rest_client
256 self._rest_client.create_service()
257
Xixuan Wu55d38c52019-05-21 14:26:23 -0700258 def query(self, query_str):
259 """Query bigquery.
Xixuan Wu7d142a92019-04-26 12:03:02 -0700260
261 Args:
Xixuan Wu55d38c52019-05-21 14:26:23 -0700262 query_str: A string used to query Bigquery.
Xixuan Wu7d142a92019-04-26 12:03:02 -0700263
264 Returns:
265 A json dict returned by API bigquery.jobs.query, e.g.
266 # {...,
267 # "rows": [
268 # {
269 # "f": [ # field
270 # {
271 # "v": # value
Xixuan Wu55d38c52019-05-21 14:26:23 -0700272 # },
273 # {
274 # "v": # value
275 # },
276 # ...
Xixuan Wu7d142a92019-04-26 12:03:02 -0700277 # ]
278 # }
279 # ]
280 # }
281 """
282 query_data = {
Xixuan Wu55d38c52019-05-21 14:26:23 -0700283 'query': query_str,
284 'useLegacySql': False,
Xixuan Wu7d142a92019-04-26 12:03:02 -0700285 }
286 return self._rest_client.service.jobs().query(
Xixuan Wu55d38c52019-05-21 14:26:23 -0700287 projectId=self.PROJECT_TO_RUN_BIGQUERY_JOB,
288 fields='rows',
Xixuan Wu7d142a92019-04-26 12:03:02 -0700289 body=query_data).execute()
Xixuan Wu55d38c52019-05-21 14:26:23 -0700290
291
292class CrOSSwarmingBigqueryClient(BigqueryRestClient):
293 """REST client for chromeos-swarming Bigquery API."""
294
295 def get_past_skylab_job_nums(self, hours):
296 """Query the count of jobs kicked off to chromeos-swarming in past hours.
297
298 Args:
299 hours: An integer.
300
301 Returns:
302 An integer.
303 """
304 query_str = """
305 SELECT
306 COUNT(*)
307 FROM
308 `chromeos-swarming.swarming.task_requests` AS r
309 WHERE
310 'user:suite_scheduler' in UNNEST(r.tags) and
311 create_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(),
312 INTERVAL %d HOUR);
313 """
314 res = self.query(query_str % hours)
315 try:
316 return int(_parse_bq_job_query(res)[0][0])
317 except (ValueError, KeyError) as e:
318 logging.debug('The returned json: \n%r', res)
319 logging.exception(str(e))
320 raise
321
322
323def _parse_bq_job_query(json_input):
324 """Parse response from API bigquery.jobs.query.
325
326 Args:
327 json_input: a dict, representing jsons returned by query API.
328
329 Returns:
330 A 2D string matrix: [rows[columns]], or None if no result.
331 E.g. Input:
332 "rows": [
333 {
334 "f": [ # field
335 {
336 "v": 'foo1',
337 },
338 {
339 "v": 'foo2',
340 }
341 ]
342 }
343 {
344 "f": [ # field
345 {
346 "v": 'bar1',
347 },
348 {
349 "v": 'bar2',
350 }
351 ]
352 }
353 ]
354 => Output: [['foo1', 'foo2'], ['bar1', 'bar2']]
355 """
356 if 'rows' not in json_input:
357 return None
358
359 res = []
360 for r in json_input['rows']:
361 rc = []
362 for c in r['f']:
363 rc.append(c['v'])
364
365 res.append(rc)
366
367 return res