blob: 0d896490eabbb470dcabecbe935c413dda639bc6 [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 Wuf856ff12019-05-21 14:09:38 -07008import ast
Xixuan Wu5d6063e2017-09-05 16:15:07 -07009import httplib2
Xixuan Wu55d38c52019-05-21 14:26:23 -070010import logging
Xixuan Wuf856ff12019-05-21 14:09:38 -070011import re
Sean McAllister7d021782021-07-15 08:59:57 -060012import time
xixuan878b1eb2017-03-20 15:58:17 -070013
14import apiclient
Xixuan Wuf856ff12019-05-21 14:09:38 -070015import build_lib
xixuan878b1eb2017-03-20 15:58:17 -070016import constants
17import file_getter
Xixuan Wuf856ff12019-05-21 14:09:38 -070018import global_config
19import time_converter
xixuan878b1eb2017-03-20 15:58:17 -070020
21from oauth2client import service_account
22from oauth2client.contrib import appengine
23
Sean McAllister7d021782021-07-15 08:59:57 -060024RETRY_LIMIT = 3
25
xixuan878b1eb2017-03-20 15:58:17 -070026
27class RestClientError(Exception):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070028 """Raised when there is a general error."""
xixuan878b1eb2017-03-20 15:58:17 -070029
30
31class NoServiceRestClientError(RestClientError):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070032 """Raised when there is no ready service for a google API."""
xixuan878b1eb2017-03-20 15:58:17 -070033
34
35class BaseRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070036 """Base class of REST client for google APIs."""
xixuan878b1eb2017-03-20 15:58:17 -070037
Xixuan Wu5d6063e2017-09-05 16:15:07 -070038 def __init__(self, scopes, service_name, service_version):
39 """Initialize a REST client to connect to a google API.
xixuan878b1eb2017-03-20 15:58:17 -070040
Xixuan Wu5d6063e2017-09-05 16:15:07 -070041 Args:
42 scopes: the scopes of the to-be-connected API.
43 service_name: the service name of the to-be-connected API.
44 service_version: the service version of the to-be-connected API.
45 """
46 self.running_env = constants.environment()
47 self.scopes = scopes
48 self.service_name = service_name
49 self.service_version = service_version
xixuan878b1eb2017-03-20 15:58:17 -070050
Xixuan Wu5d6063e2017-09-05 16:15:07 -070051 @property
52 def service(self):
53 if not self._service:
54 raise NoServiceRestClientError('No service created for calling API')
xixuan878b1eb2017-03-20 15:58:17 -070055
Xixuan Wu5d6063e2017-09-05 16:15:07 -070056 return self._service
xixuan878b1eb2017-03-20 15:58:17 -070057
Tim Baina602d462022-05-13 21:08:56 +000058 def create_service(self, discovery_url=None, http_timeout_seconds=30):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070059 """Create the service for a google API."""
60 self._init_credentials()
Tim Baina602d462022-05-13 21:08:56 +000061
62 logging.debug('HTTP timeout seconds: %s', http_timeout_seconds)
Xixuan Wu5d6063e2017-09-05 16:15:07 -070063 # Explicitly specify timeout for http to avoid DeadlineExceededError.
64 # It's used for services like AndroidBuild API, which raise such error
65 # when being triggered too many calls in a short time frame.
66 # http://stackoverflow.com/questions/14698119/httpexception-deadline-exceeded-while-waiting-for-http-response-from-url-dead
Tim Baina602d462022-05-13 21:08:56 +000067 http_auth = self._credentials.authorize(
68 httplib2.Http(timeout=http_timeout_seconds))
Xixuan Wu5d6063e2017-09-05 16:15:07 -070069 if discovery_url is None:
70 self._service = apiclient.discovery.build(
71 self.service_name, self.service_version,
72 http=http_auth)
73 else:
74 self._service = apiclient.discovery.build(
75 self.service_name, self.service_version, http=http_auth,
76 discoveryServiceUrl=discovery_url)
xixuan878b1eb2017-03-20 15:58:17 -070077
Xixuan Wu5d6063e2017-09-05 16:15:07 -070078 def _init_credentials(self):
79 """Initialize the credentials for a google API."""
80 if (self.running_env == constants.RunningEnv.ENV_STANDALONE or
81 self.running_env == constants.RunningEnv.ENV_DEVELOPMENT_SERVER):
82 # Running locally
83 service_credentials = service_account.ServiceAccountCredentials
84 self._credentials = service_credentials.from_json_keyfile_name(
Xixuan Wu26d06e02017-09-20 14:50:28 -070085 file_getter.STAGING_CLIENT_SECRETS_FILE, self.scopes)
Xixuan Wu5d6063e2017-09-05 16:15:07 -070086 else:
87 # Running in app-engine production
88 self._credentials = appengine.AppAssertionCredentials(self.scopes)
xixuan878b1eb2017-03-20 15:58:17 -070089
90
91class AndroidBuildRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070092 """REST client for android build API."""
xixuan878b1eb2017-03-20 15:58:17 -070093
Xixuan Wu5d6063e2017-09-05 16:15:07 -070094 def __init__(self, rest_client):
95 """Initialize a REST client for connecting to Android Build API."""
96 self._rest_client = rest_client
97 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -070098
Xixuan Wu5d6063e2017-09-05 16:15:07 -070099 def get_latest_build_id(self, branch, target):
100 """Get the latest build id for a given branch and target.
xixuan878b1eb2017-03-20 15:58:17 -0700101
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700102 Args:
103 branch: an android build's branch
104 target: an android build's target
xixuan878b1eb2017-03-20 15:58:17 -0700105
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700106 Returns:
107 A string representing latest build id.
108 """
109 request = self._rest_client.service.build().list(
110 buildType='submitted',
111 branch=branch,
112 target=target,
113 successful=True,
114 maxResults=1)
115 builds = request.execute(num_retries=10)
116 if not builds or not builds['builds']:
117 return None
xixuan878b1eb2017-03-20 15:58:17 -0700118
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700119 return builds['builds'][0]['buildId']
xixuan878b1eb2017-03-20 15:58:17 -0700120
121
122class StorageRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700123 """REST client for google storage API."""
xixuan878b1eb2017-03-20 15:58:17 -0700124
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700125 def __init__(self, rest_client):
126 """Initialize a REST client for connecting to Google storage API."""
127 self._rest_client = rest_client
128 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -0700129
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700130 def read_object(self, bucket, object_path):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700131 """Read the contents of input_object in input_bucket.
xixuan878b1eb2017-03-20 15:58:17 -0700132
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700133 Args:
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700134 bucket: A string to indicate the bucket for fetching the object.
135 e.g. constants.StorageBucket.PROD_SUITE_SCHEDULER
136 object_path: A string to indicate the path of the object to read the
137 contents.
xixuan878b1eb2017-03-20 15:58:17 -0700138
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700139 Returns:
140 the stripped string contents of the input object.
xixuan878b1eb2017-03-20 15:58:17 -0700141
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700142 Raises:
143 apiclient.errors.HttpError
144 """
145 req = self._rest_client.service.objects().get_media(
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700146 bucket=bucket,
147 object=object_path)
148 return req.execute()
149
150 def upload_object(self, bucket, src_object_path, dest_object_path):
151 """Upload object_path to input_bucket.
152
153 Args:
154 bucket: A string to indicate the bucket for the object to be uploaded to.
155 src_object_path: A string the full path of the object to upload.
156 dest_object_path: A string path inside bucket to upload to.
157
158 Returns:
159 A dict of uploaded object info.
160
161 Raises:
162 apiclient.errors.HttpError
163 """
164 req = self._rest_client.service.objects().insert(
165 bucket=bucket,
166 name=dest_object_path,
167 media_body=src_object_path,
168 media_mime_type='text/plain',
169 )
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700170 return req.execute()
xixuan878b1eb2017-03-20 15:58:17 -0700171
172
173class CalendarRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700174 """Class of REST client for google calendar API."""
xixuan878b1eb2017-03-20 15:58:17 -0700175
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700176 def __init__(self, rest_client):
177 """Initialize a REST client for connecting to Google calendar API."""
178 self._rest_client = rest_client
179 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -0700180
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700181 def add_event(self, calendar_id, input_event):
182 """Add events of a given calendar.
xixuan878b1eb2017-03-20 15:58:17 -0700183
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700184 Args:
185 calendar_id: the ID of the given calendar.
186 input_event: the event to be added.
187 """
188 self._rest_client.service.events().insert(
189 calendarId=calendar_id,
190 body=input_event).execute()
xixuan878b1eb2017-03-20 15:58:17 -0700191
192
Xixuan Wu6f117e92017-10-27 10:51:58 -0700193class StackdriverRestClient(object):
194 """REST client for google storage API."""
195
196 def __init__(self, rest_client):
197 """Initialize a REST client for connecting to Google storage API."""
198 self._rest_client = rest_client
199 self._rest_client.create_service()
200
201 def read_logs(self, request):
Xinan Lin318cf752019-07-19 14:50:23 -0700202 # project_id, page_size, order_by, query_filter=''):
Xixuan Wu6f117e92017-10-27 10:51:58 -0700203 """Read the logs of the project_id based on all filters.
204
205 Args:
206 request: a request dict generated by
207 stackdriver_lib.form_logging_client_request.
208
209 Returns:
210 A json object, can be parsed by
211 stackdriver_lib.parse_logging_client_response.
212
213 Raises:
214 apiclient.errors.HttpError
215 """
216 req = self._rest_client.service.entries().list(
217 fields='entries/protoPayload', body=request)
218 return req.execute()
219
220
xixuan878b1eb2017-03-20 15:58:17 -0700221class SwarmingRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700222 """REST client for swarming proxy API."""
xixuan878b1eb2017-03-20 15:58:17 -0700223
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700224 DISCOVERY_URL_PATTERN = '%s/discovery/v1/apis/%s/%s/rest'
xixuan878b1eb2017-03-20 15:58:17 -0700225
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700226 def __init__(self, rest_client, service_url):
227 self._rest_client = rest_client
228 discovery_url = self.DISCOVERY_URL_PATTERN % (
229 service_url, rest_client.service_name, rest_client.service_version)
230 self._rest_client.create_service(discovery_url=discovery_url)
xixuan878b1eb2017-03-20 15:58:17 -0700231
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700232 def create_task(self, request):
233 """Create new task.
xixuan878b1eb2017-03-20 15:58:17 -0700234
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700235 Args:
236 request: a json-compatible dict expected by swarming server.
237 See _to_raw_request's output in swarming_lib.py for details.
xixuan878b1eb2017-03-20 15:58:17 -0700238
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700239 Returns:
240 A json dict returned by API task.new.
241 """
242 return self._rest_client.service.tasks().new(
243 fields='request,task_id', body=request).execute()
xixuan878b1eb2017-03-20 15:58:17 -0700244
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700245 def get_task_result(self, task_id):
246 """Get task results by a given task_id.
xixuan878b1eb2017-03-20 15:58:17 -0700247
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700248 Args:
249 task_id: A string, represents task id.
250
251 Returns:
252 A json dict returned by API task.result.
253 """
254 return self._rest_client.service.task().result(
255 task_id=task_id).execute()
Xixuan Wu7d142a92019-04-26 12:03:02 -0700256
257
258class BigqueryRestClient(object):
259 """Class of REST client for Bigquery API."""
260
Xixuan Wu55d38c52019-05-21 14:26:23 -0700261 PROJECT_TO_RUN_BIGQUERY_JOB = 'google.com:suite-scheduler'
Tim Baina602d462022-05-13 21:08:56 +0000262 QUERY_TIMEOUT_SECONDS = 200
Xixuan Wu55d38c52019-05-21 14:26:23 -0700263
Xinan Linc9f01152020-02-05 22:05:13 -0800264 def __init__(self, rest_client, project=None, dataset=None, table=None):
Xixuan Wu7d142a92019-04-26 12:03:02 -0700265 """Initialize a REST client for connecting to Bigquery API."""
266 self._rest_client = rest_client
Tim Baina602d462022-05-13 21:08:56 +0000267 # We always want to use an HTTP timeout that's greater than the
268 # underlying query timeout.
269 self._rest_client.create_service(
270 http_timeout_seconds=self.QUERY_TIMEOUT_SECONDS + 5)
Xinan Linc9f01152020-02-05 22:05:13 -0800271 self.project = project
272 self.dataset = dataset
273 self.table = table
Xixuan Wu7d142a92019-04-26 12:03:02 -0700274
Xixuan Wu55d38c52019-05-21 14:26:23 -0700275 def query(self, query_str):
276 """Query bigquery.
Xixuan Wu7d142a92019-04-26 12:03:02 -0700277
278 Args:
Xixuan Wu55d38c52019-05-21 14:26:23 -0700279 query_str: A string used to query Bigquery.
Xixuan Wu7d142a92019-04-26 12:03:02 -0700280
281 Returns:
282 A json dict returned by API bigquery.jobs.query, e.g.
283 # {...,
284 # "rows": [
285 # {
286 # "f": [ # field
287 # {
288 # "v": # value
Xixuan Wu55d38c52019-05-21 14:26:23 -0700289 # },
290 # {
291 # "v": # value
292 # },
293 # ...
Xixuan Wu7d142a92019-04-26 12:03:02 -0700294 # ]
295 # }
Xixuan Wuf856ff12019-05-21 14:09:38 -0700296 # ...
Xixuan Wu7d142a92019-04-26 12:03:02 -0700297 # ]
298 # }
299 """
Tim Baina602d462022-05-13 21:08:56 +0000300 query_timeout_ms = self.QUERY_TIMEOUT_SECONDS * 1000
301 logging.debug('BQ query timeout seconds: %s', query_timeout_ms)
Xixuan Wu7d142a92019-04-26 12:03:02 -0700302 query_data = {
Xixuan Wu55d38c52019-05-21 14:26:23 -0700303 'query': query_str,
304 'useLegacySql': False,
Tim Baina602d462022-05-13 21:08:56 +0000305 'timeoutMs': query_timeout_ms,
Xixuan Wu7d142a92019-04-26 12:03:02 -0700306 }
Sean McAllister7d021782021-07-15 08:59:57 -0600307
308 for cnt in range(RETRY_LIMIT+1):
309 try:
310 return self._rest_client.service.jobs().query(
311 projectId=self.PROJECT_TO_RUN_BIGQUERY_JOB,
312 fields='rows',
313 body=query_data).execute()
314 except apiclient.errors.HttpError as ex:
315 status = ex.resp.status
316 if status in [500, 502, 503, 504]:
317 if cnt < RETRY_LIMIT:
318 logging.warning("Got response status %d, retrying" % status)
319 time.sleep(5)
320 else:
321 logging.error(
322 "Retry limit of %d hit communicating with BigQuery" % RETRY_LIMIT)
323 raise
324
Xixuan Wu55d38c52019-05-21 14:26:23 -0700325
Xinan Linc9f01152020-02-05 22:05:13 -0800326 def insert(self, rows):
327 """Insert rows to specified Bigquery table.
328
329 Args:
330 rows: list of json objects.
331
332 Raise:
333 RestClientError: if project/dataset/table is not defined.
334 """
335 if not any([self.project, self.dataset, self.table]):
336 raise RestClientError('Project, dataset, table should be all set.'
337 'Got project:%s, dataset:%s, table:%s' %
338 (self.project, self.dataset, self.table))
339 body = {
340 'kind': 'bigquery#tableDataInsertAllRequest',
341 'rows': rows,
342 }
343 request = self._rest_client.service.tabledata().insertAll(
344 projectId=self.project,
345 datasetId=self.dataset,
346 tableId=self.table,
347 body=body)
348 response = request.execute(num_retries=3)
349 if response.get('insertErrors'):
350 logging.error('InsertRequest reported errors: %r',
351 response.get('insertErrors'))
352 return False
353
354 return True
Xixuan Wu55d38c52019-05-21 14:26:23 -0700355
Xinan Lin80a9d932019-10-17 09:24:43 -0700356class CrOSTestPlatformBigqueryClient(BigqueryRestClient):
357 """REST client for cros_test_platform builder Bigquery API."""
Xixuan Wu55d38c52019-05-21 14:26:23 -0700358
Xinan Lin80a9d932019-10-17 09:24:43 -0700359 def get_past_job_nums(self, hours):
360 """Query the count of the jobs kicked off to cros_test_platform.
Xixuan Wu55d38c52019-05-21 14:26:23 -0700361
362 Args:
363 hours: An integer.
364
365 Returns:
366 An integer.
367 """
368 query_str = """
369 SELECT
370 COUNT(*)
371 FROM
Xinan Lin80a9d932019-10-17 09:24:43 -0700372 `cr-buildbucket.chromeos.builds`
Xixuan Wu55d38c52019-05-21 14:26:23 -0700373 WHERE
Xinan Lin80a9d932019-10-17 09:24:43 -0700374 created_by = 'user:suite-scheduler.google.com@appspot.gserviceaccount.com'
375 and create_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL %d HOUR);
Xixuan Wu55d38c52019-05-21 14:26:23 -0700376 """
377 res = self.query(query_str % hours)
378 try:
379 return int(_parse_bq_job_query(res)[0][0])
380 except (ValueError, KeyError) as e:
381 logging.debug('The returned json: \n%r', res)
382 logging.exception(str(e))
383 raise
384
385
Xixuan Wuf856ff12019-05-21 14:09:38 -0700386class BuildBucketBigqueryClient(BigqueryRestClient):
387 """Rest client for buildbucket Bigquery API."""
388
Xinan Lin028f9582019-12-11 10:55:33 -0800389 def get_latest_passed_firmware_builds(self):
390 """Get artifact link of the latest passed firmware builds for board.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700391
Xinan Lin028f9582019-12-11 10:55:33 -0800392 The query returns the latest firmware build for the combination of
393 board and build spec, which is cros or firmware. No restriction set
394 in the query, so it should return all available builds.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700395
396 Returns:
Xinan Lin028f9582019-12-11 10:55:33 -0800397 A list of (spec, board, firmware_artifact_link).
Xixuan Wuf856ff12019-05-21 14:09:38 -0700398 """
399 query_str = """
400 SELECT
Xinan Lin028f9582019-12-11 10:55:33 -0800401 spec,
402 board,
403 /*
404 * Firmware builds may contain artifacts for multiple boards in a
405 * single build - each in a separate directory.
406 */
407 IF(spec = 'firmware', CONCAT(artifact, '/', board), artifact) as artifact
Xixuan Wuf856ff12019-05-21 14:09:38 -0700408 FROM
Xinan Lin028f9582019-12-11 10:55:33 -0800409 (
410 SELECT
411 spec,
412 board,
413 artifact,
414 RANK() OVER (PARTITION BY spec, board ORDER BY end_time DESC) AS rank
415 FROM
416 (
417 SELECT
418 /*
419 * build_config is a string contains the board and build type.
420 * For Cros build, it has the form of "BoardName-release", while
421 * the firmware config shows like "firmware-BoardName-[1]-firmwarebranch".
422 * [1] is the firmware ver.
423 */
424 IF(prefix = 'firmware', 'firmware', 'cros') AS spec,
Jeremy Bettisd299c732022-02-23 12:41:03 -0700425 IF(prefix = 'firmware', COALESCE(
426 SPLIT(firmware_tarball, '/') [OFFSET(0)],
427 SPLIT(build_config, '-') [OFFSET(1)]
428 ), cros_prefix) AS board,
Xinan Lin028f9582019-12-11 10:55:33 -0800429 artifact,
430 end_time
431 FROM
432 (
433 SELECT
434 SPLIT(build_config, '-') [OFFSET(0)] AS prefix,
Jeremy Bettisd299c732022-02-23 12:41:03 -0700435 build_config,
Xinan Lin028f9582019-12-11 10:55:33 -0800436 REGEXP_EXTRACT(
437 build_config, r"(^[a-zA-Z0-9_.+-]+)-release"
438 ) as cros_prefix,
439 end_time,
Jeremy Bettisd299c732022-02-23 12:41:03 -0700440 artifact,
441 JSON_VALUE(fbb, '$') as firmware_tarball
Xinan Lin028f9582019-12-11 10:55:33 -0800442 FROM
443 (
444 SELECT
Sean McAllister53dd3d82021-05-18 15:15:14 -0600445 COALESCE(
446 JSON_EXTRACT_SCALAR(
447 output.properties, '$.artifact_link'
448 ),
449 FORMAT('gs://%s/%s',
450 JSON_EXTRACT_SCALAR(output.properties, '$.artifacts.gs_bucket'),
451 JSON_EXTRACT_SCALAR(output.properties, '$.artifacts.gs_path'))
Sean McAllister3fb4cec2021-04-20 22:38:38 +0000452 ) as artifact,
Sean McAllister53dd3d82021-05-18 15:15:14 -0600453 COALESCE(
454 JSON_EXTRACT_SCALAR(
455 output.properties, '$.cbb_config'
456 ),
457 builder.builder
Xinan Lin028f9582019-12-11 10:55:33 -0800458 ) as build_config,
Jeremy Bettisd299c732022-02-23 12:41:03 -0700459 end_time,
460 JSON_EXTRACT_ARRAY(output.properties, '$.artifacts.files_by_artifact.FIRMWARE_TARBALL') as firmware_by_board
Xinan Lin028f9582019-12-11 10:55:33 -0800461 FROM `cr-buildbucket.chromeos.completed_builds_BETA`
462 WHERE
463 status = 'SUCCESS'
464 AND JSON_EXTRACT_SCALAR(
465 output.properties, '$.suite_scheduling'
466 ) = 'True'
Jeremy Bettisd299c732022-02-23 12:41:03 -0700467 ) LEFT JOIN UNNEST(firmware_by_board) as fbb
Xinan Lin028f9582019-12-11 10:55:33 -0800468 )
469 )
470 )
471 WHERE rank = 1
Xixuan Wuf856ff12019-05-21 14:09:38 -0700472 """
Xinan Lin028f9582019-12-11 10:55:33 -0800473 res = self.query(query_str)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700474 res = _parse_bq_job_query(res)
475 if res is None:
476 return None
Xinan Lin028f9582019-12-11 10:55:33 -0800477 logging.info('Fetched the latest artifact links: %s',
478 [row[2] for row in res])
479 return res
Xinan Lin318cf752019-07-19 14:50:23 -0700480
Jack Neus8f0edb42022-03-17 20:21:39 +0000481 # TODO(b/225382624): Remove this when Rubik is the only source of builds.
Xinan Lin71eeeb02020-03-10 17:37:12 -0700482 def get_passed_builds(self, earliest_end_time, latest_end_time, event_type):
Xinan Linea1efcb2019-12-30 23:46:42 -0800483 """Get passed builds inside a given time span.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700484
Xinan Lin3330d672020-03-03 14:52:36 -0800485 BigQuery does not guarantee the inserted time of rows. A new build
486 may not get inserted when suite scheduler runs the query. To avoid
Xinan Lin71eeeb02020-03-10 17:37:12 -0700487 it, we scan each time span twice:
Xinan Lin3330d672020-03-03 14:52:36 -0800488 - the first run catches the new build from earliest_end_time to
489 latest_end_time, and inserts the result to a temp BQ table.
490 - the second run checks the build from (earliest_end_time - 1Day)
491 to (latest_end_time - 1Day) plus (earliest_end_time to
492 latest_end_time). The query returns the build which does not
493 appear in the temp table. Thus, if a build was not fetched by the
Xinan Lin71eeeb02020-03-10 17:37:12 -0700494 first run, we still could schedule test on it at most 1 day later
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700495 for new_build events.
496 Weekly and nightly events do not need this arrangement because
497 they do not cover every single build.
498
Xinan Lin3330d672020-03-03 14:52:36 -0800499
Xixuan Wuf856ff12019-05-21 14:09:38 -0700500 Args:
Xinan Linea1efcb2019-12-30 23:46:42 -0800501 earliest_end_time: a datetime.datetime object in UTC.
502 latest_end_time: a datetime.datetime object in UTC.
Xinan Lin71eeeb02020-03-10 17:37:12 -0700503 event_type: a string of event type. It could be one of
504 [WEEKLY|NIGHTLY|new_build].
Xixuan Wuf856ff12019-05-21 14:09:38 -0700505
506 Returns:
507 A list of build_lib.BuildInfo objects.
508 """
Xinan Lin761b0c52020-03-25 17:31:57 -0700509 base_query_template = """
Sean McAllister53dd3d82021-05-18 15:15:14 -0600510 WITH builds AS
511 (SELECT
512 COALESCE(
513 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
514 JSON_EXTRACT_SCALAR(output.properties, '$.build_targets[0].name')
515 ) AS board,
516 COALESCE(
517 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
518 REPLACE(
519 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(0)], 'R', '')
520 ) AS milestone,
521 COALESCE(
522 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version'),
523 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(1)]
524 ) AS platform,
525 COALESCE(
526 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config'),
527 builder.builder
528 ) AS build_config,
529
530 -- Time info
531 end_time as build_end_time,
532 CURRENT_TIMESTAMP() as inserted_time,
533 FROM `cr-buildbucket.chromeos.builds`
534 WHERE
535 status = 'SUCCESS'
536 AND JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True')
537
538 SELECT
539 *,
540 '{0}' as event_type
541 FROM builds
542 WHERE
543 board IS NOT NULL
Xixuan Wuf856ff12019-05-21 14:09:38 -0700544 """
Jack Neus8f0edb42022-03-17 20:21:39 +0000545 return self._get_passed_builds(base_query_template, earliest_end_time, latest_end_time, event_type)
546
547 def get_passed_rubik_builds(self, earliest_end_time, latest_end_time, event_type):
548 """Get passed Rubik builds inside a given time span.
549
550 BigQuery does not guarantee the inserted time of rows. A new build
551 may not get inserted when suite scheduler runs the query. To avoid
552 it, we scan each time span twice:
553 - the first run catches the new build from earliest_end_time to
554 latest_end_time, and inserts the result to a temp BQ table.
555 - the second run checks the build from (earliest_end_time - 1Day)
556 to (latest_end_time - 1Day) plus (earliest_end_time to
557 latest_end_time). The query returns the build which does not
558 appear in the temp table. Thus, if a build was not fetched by the
559 first run, we still could schedule test on it at most 1 day later
560 for new_build events.
561 Weekly and nightly events do not need this arrangement because
562 they do not cover every single build.
563
564
565 Args:
566 earliest_end_time: a datetime.datetime object in UTC.
567 latest_end_time: a datetime.datetime object in UTC.
568 event_type: a string of event type. It could be one of
569 [WEEKLY|NIGHTLY|new_build].
570
571 Returns:
572 A list of build_lib.BuildInfo objects.
573 """
574 base_query_template = """
575 WITH builds AS (
576 SELECT
577 JSON_EXTRACT_SCALAR(input.properties, '$.build_target.name') AS board,
Jack Neus6c270dd2022-03-18 20:02:48 +0000578 JSON_EXTRACT_SCALAR(output.properties, '$.target_versions.milestoneVersion') AS milestone,
Jack Neus8f0edb42022-03-17 20:21:39 +0000579 JSON_EXTRACT_SCALAR(output.properties, '$.target_versions.platformVersion') AS platform,
Jack Neus447653b2022-03-23 18:07:46 +0000580 CONCAT(JSON_EXTRACT_SCALAR(input.properties, '$.build_target.name'), "-release") AS build_config,
Jack Neus8f0edb42022-03-17 20:21:39 +0000581 -- Time info
582 end_time as build_end_time,
583 CURRENT_TIMESTAMP() as inserted_time,
584 FROM `cr-buildbucket.chromeos.builds`
585 WHERE
586 status = 'SUCCESS' AND
Jack Neus50603cf2022-03-18 20:30:48 +0000587 JSON_EXTRACT_SCALAR(input.properties, '$.recipe') = 'build_release' AND
588 builder.builder NOT LIKE "staging-%"
Jack Neus8f0edb42022-03-17 20:21:39 +0000589 )
590 SELECT
591 *,
592 '{0}' as event_type
593 FROM builds
594 WHERE
595 board IS NOT NULL
596 """
597 return self._get_passed_builds(base_query_template, earliest_end_time, latest_end_time, event_type)
598
599 def _get_passed_builds(self, base_query_template, earliest_end_time, latest_end_time, event_type):
600 """Get passed builds inside a given time span.
601
602 BigQuery does not guarantee the inserted time of rows. A new build
603 may not get inserted when suite scheduler runs the query. To avoid
604 it, we scan each time span twice:
605 - the first run catches the new build from earliest_end_time to
606 latest_end_time, and inserts the result to a temp BQ table.
607 - the second run checks the build from (earliest_end_time - 1Day)
608 to (latest_end_time - 1Day) plus (earliest_end_time to
609 latest_end_time). The query returns the build which does not
610 appear in the temp table. Thus, if a build was not fetched by the
611 first run, we still could schedule test on it at most 1 day later
612 for new_build events.
613 Weekly and nightly events do not need this arrangement because
614 they do not cover every single build.
615
616
617 Args:
618 base_query_template: base query to use to find release builds.
619 earliest_end_time: a datetime.datetime object in UTC.
620 latest_end_time: a datetime.datetime object in UTC.
621 event_type: a string of event type. It could be one of
622 [WEEKLY|NIGHTLY|new_build].
623
624 Returns:
625 A list of build_lib.BuildInfo objects.
626 """
Xinan Lin761b0c52020-03-25 17:31:57 -0700627 base_query_str = base_query_template.format(event_type)
Xinan Lin71eeeb02020-03-10 17:37:12 -0700628 earliest_end_time_str = earliest_end_time.strftime(
629 time_converter.TIME_FORMAT)
630 latest_end_time_str = latest_end_time.strftime(time_converter.TIME_FORMAT)
631 project_id = constants.AppID.STAGING_APP
632 if constants.environment() == constants.RunningEnv.ENV_PROD:
633 project_id = constants.application_id()
Xinan Lin3330d672020-03-03 14:52:36 -0800634
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700635 if event_type == 'new_build':
636 insert_passed_builds = """
637 INSERT
638 `google.com:{0}.builds.passed_builds`(
639 board,
640 milestone,
641 platform,
642 build_config,
643 build_end_time,
644 inserted_time,
645 event_type
646 ) {1}
Sean McAllister909997a2021-05-19 13:28:25 -0600647 AND build_end_time > '{2}'
648 AND build_end_time < '{3}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700649 """
Sean McAllister909997a2021-05-19 13:28:25 -0600650
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700651 # Insert the currently visible builds to BQ.
652 logging.info(
653 'Insert the visible passed builds '
654 'between %s and %s to BQ.', earliest_end_time_str,
655 latest_end_time_str)
Sean McAllister909997a2021-05-19 13:28:25 -0600656
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700657 self.query(
658 insert_passed_builds.format(project_id, base_query_str,
659 earliest_end_time_str,
660 latest_end_time_str))
Xixuan Wuf856ff12019-05-21 14:09:38 -0700661
Sean McAllister53dd3d82021-05-18 15:15:14 -0600662 query_template = _passed_build_query_template(event_type)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700663 query_str = query_template.format(base_query_str, earliest_end_time_str,
664 latest_end_time_str, project_id)
Xinan Lin3330d672020-03-03 14:52:36 -0800665 if global_config.GAE_TESTING:
Xinan Lin71eeeb02020-03-10 17:37:12 -0700666 query_str += 'LIMIT 10'
667 logging.info('Getting passed builds finished between %s and %s',
668 earliest_end_time_str, latest_end_time_str)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700669 res = self.query(query_str)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700670 res = _parse_bq_job_query(res)
671 if res is None:
672 return []
673
674 build_infos = []
675 for board, milestone, platform, build_config in res:
Xixuan Wuf856ff12019-05-21 14:09:38 -0700676 build_infos.append(
677 build_lib.BuildInfo(board, None, milestone, platform, build_config))
678
679 return build_infos
680
Xinan Lin71eeeb02020-03-10 17:37:12 -0700681 def get_relaxed_passed_builds(self, earliest_end_time, latest_end_time,
682 event_type):
Jared Loucksa676b5d2022-04-15 15:18:44 -0600683 """Get builds with successful UploadTestArtifacts stages in a given span.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700684
Xinan Lin71eeeb02020-03-10 17:37:12 -0700685 Same as get_passed_builds, we run the query twice to ensure we
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700686 fetched all builds from BQ for new_build event.
Xinan Lin3330d672020-03-03 14:52:36 -0800687
Xixuan Wuf856ff12019-05-21 14:09:38 -0700688 Args:
Xinan Linea1efcb2019-12-30 23:46:42 -0800689 earliest_end_time: a datetime.datetime object in UTC.
690 latest_end_time: a datetime.datetime object in UTC.
Xinan Lin71eeeb02020-03-10 17:37:12 -0700691 event_type: a string of event type.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700692
693 Returns:
694 A list of build_lib.BuildInfo objects.
695 """
Xinan Lin761b0c52020-03-25 17:31:57 -0700696 base_query_template = """
Sean McAllister53dd3d82021-05-18 15:15:14 -0600697 WITH builds AS
698 (SELECT
699 COALESCE(
700 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
701 JSON_EXTRACT_SCALAR(output.properties, '$.build_targets[0].name')
702 ) AS board,
703 COALESCE(
704 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
705 REPLACE(
706 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(0)], 'R', '')
707 ) AS milestone,
708 COALESCE(
709 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version'),
710 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(1)]
711 ) AS platform,
712 COALESCE(
713 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config'),
714 builder.builder
715 ) AS build_config,
716
717 step.name AS stage_name,
718
719 -- Time info
720 build.end_time as build_end_time,
721 CURRENT_TIMESTAMP() as inserted_time,
722 FROM `cr-buildbucket.chromeos.builds` build,
723 UNNEST(build.steps) AS step
724 WHERE
725 build.status != 'SUCCESS'
Jared Loucksa676b5d2022-04-15 15:18:44 -0600726 AND step.name = 'UploadTestArtifacts'
Sean McAllister53dd3d82021-05-18 15:15:14 -0600727 AND step.status = 'SUCCESS'
728 AND JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True')
729
730 SELECT
731 *,
732 '{0}' AS event_type
733 FROM
734 builds
735 WHERE board IS NOT NULL
Xixuan Wuf856ff12019-05-21 14:09:38 -0700736 """
Sean McAllister53dd3d82021-05-18 15:15:14 -0600737
Xinan Lin761b0c52020-03-25 17:31:57 -0700738 base_query_str = base_query_template.format(event_type)
Xinan Lin71eeeb02020-03-10 17:37:12 -0700739 earliest_end_time_str = earliest_end_time.strftime(
740 time_converter.TIME_FORMAT)
741 latest_end_time_str = latest_end_time.strftime(time_converter.TIME_FORMAT)
742 project_id = constants.AppID.STAGING_APP
743 if constants.environment() == constants.RunningEnv.ENV_PROD:
744 project_id = constants.application_id()
Xinan Lin3330d672020-03-03 14:52:36 -0800745
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700746 if event_type == 'new_build':
747 insert_relaxed_builds = """
748 INSERT
749 `google.com:{0}.builds.relaxed_builds`(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700750 stage_name,
751 board,
752 milestone,
753 platform,
754 build_config,
755 build_end_time,
756 inserted_time,
757 event_type
758 ) {1}
Sean McAllister909997a2021-05-19 13:28:25 -0600759 AND build_end_time > '{2}'
760 AND build_end_time < '{3}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700761 """
762 logging.info(
763 'Insert the visible relaxed builds '
764 'between %s and %s to BQ.', earliest_end_time_str,
765 latest_end_time_str)
Sean McAllister909997a2021-05-19 13:28:25 -0600766
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700767 self.query(
768 insert_relaxed_builds.format(project_id, base_query_str,
769 earliest_end_time_str,
770 latest_end_time_str))
Xixuan Wuf856ff12019-05-21 14:09:38 -0700771
Sean McAllister53dd3d82021-05-18 15:15:14 -0600772 query_template = _relaxed_build_query_template(event_type)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700773 query_str = query_template.format(base_query_str, earliest_end_time_str,
774 latest_end_time_str, project_id)
Xinan Lin3330d672020-03-03 14:52:36 -0800775 if global_config.GAE_TESTING:
Xinan Lin71eeeb02020-03-10 17:37:12 -0700776 query_str += 'LIMIT 10'
777 logging.info('Getting relaxed passed builds finished between %s and %s',
778 earliest_end_time_str, latest_end_time_str)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700779 res = self.query(query_str)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700780 res = _parse_bq_job_query(res)
781 if res is None:
782 return []
783
784 build_infos = []
Sean McAllister53dd3d82021-05-18 15:15:14 -0600785 for stage_name, board, milestone, platform, build_config in res:
Xixuan Wuf856ff12019-05-21 14:09:38 -0700786 build_infos.append(
Jared Loucksa676b5d2022-04-15 15:18:44 -0600787 build_lib.BuildInfo(board, None, milestone, platform, build_config))
Xixuan Wuf856ff12019-05-21 14:09:38 -0700788
789 return build_infos
790
791
Xixuan Wu55d38c52019-05-21 14:26:23 -0700792def _parse_bq_job_query(json_input):
793 """Parse response from API bigquery.jobs.query.
794
795 Args:
796 json_input: a dict, representing jsons returned by query API.
797
798 Returns:
799 A 2D string matrix: [rows[columns]], or None if no result.
800 E.g. Input:
801 "rows": [
802 {
803 "f": [ # field
804 {
805 "v": 'foo1',
806 },
807 {
808 "v": 'foo2',
809 }
810 ]
811 }
812 {
813 "f": [ # field
814 {
815 "v": 'bar1',
816 },
817 {
818 "v": 'bar2',
819 }
820 ]
821 }
822 ]
823 => Output: [['foo1', 'foo2'], ['bar1', 'bar2']]
824 """
825 if 'rows' not in json_input:
826 return None
827
828 res = []
829 for r in json_input['rows']:
830 rc = []
831 for c in r['f']:
832 rc.append(c['v'])
833
834 res.append(rc)
835
836 return res
Xixuan Wuf856ff12019-05-21 14:09:38 -0700837
Sean McAllister53dd3d82021-05-18 15:15:14 -0600838def _passed_build_query_template(event_type):
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700839 """Wrapper to create the query template for passed builds."""
840 if event_type == 'new_build':
841 return """
842 WITH passed_builds AS
843 (
844 {0}
845 AND (
Sean McAllister6525f662021-05-21 09:33:07 -0600846 (build_end_time > TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700847 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'), INTERVAL 1 DAY)
Sean McAllister6525f662021-05-21 09:33:07 -0600848 AND build_end_time < TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700849 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'), INTERVAL 1 DAY))
Sean McAllister6525f662021-05-21 09:33:07 -0600850 OR (build_end_time > '{1}' AND build_end_time < '{2}')
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700851 )
852 )
853 SELECT
854 b.board,
855 b.milestone,
856 b.platform,
857 b.build_config,
858 FROM
859 passed_builds AS b
860 LEFT JOIN
861 `google.com:{3}.builds.passed_builds` AS r
862 ON (
863 r.board = b.board
864 AND r.milestone = b.milestone
865 AND r.build_config = b.build_config
866 AND r.platform = b.platform
867 AND r.event_type = b.event_type
868 AND r.build_end_time > TIMESTAMP_SUB(
869 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'),
870 INTERVAL 1 DAY)
871 AND r.build_end_time < TIMESTAMP_SUB(
872 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'),
873 INTERVAL 1 DAY)
874 )
875 WHERE
876 r.inserted_time is null
877 """
878 return """
879 WITH passed_builds AS
880 (
881 {0}
Sean McAllister6525f662021-05-21 09:33:07 -0600882 AND build_end_time > '{1}'
883 AND build_end_time < '{2}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700884 )
885 SELECT
886 b.board,
887 b.milestone,
888 b.platform,
889 b.build_config,
890 FROM
891 passed_builds AS b
892 """
893
894
Sean McAllister53dd3d82021-05-18 15:15:14 -0600895def _relaxed_build_query_template(event_type):
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700896 """Wrapper to create the query template for relaxed builds."""
897 if event_type == 'new_build':
898 return """
899 WITH relaxed_builds AS
900 (
901 {0}
902 AND (
Sean McAllister6525f662021-05-21 09:33:07 -0600903 (build_end_time > TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700904 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'), INTERVAL 1 DAY)
Sean McAllister6525f662021-05-21 09:33:07 -0600905 AND build_end_time < TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700906 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'), INTERVAL 1 DAY))
Sean McAllister6525f662021-05-21 09:33:07 -0600907 OR (build_end_time > '{1}' AND build_end_time < '{2}')
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700908 )
909 )
910 SELECT
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700911 b.stage_name,
912 b.board,
913 b.milestone,
914 b.platform,
915 b.build_config,
916 FROM
917 relaxed_builds AS b
918 LEFT JOIN
919 `google.com:{3}.builds.relaxed_builds` AS r
920 ON (
921 r.board = b.board
922 AND r.milestone = b.milestone
923 AND r.build_config = b.build_config
924 AND r.platform = b.platform
925 AND r.event_type = b.event_type
926 AND r.build_end_time > TIMESTAMP_SUB(
927 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'),
928 INTERVAL 1 DAY)
929 AND r.build_end_time < TIMESTAMP_SUB(
930 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'),
931 INTERVAL 1 DAY)
932 )
933 WHERE
934 r.inserted_time is null
935 """
936 return """
937 WITH relaxed_builds AS
938 (
939 {0}
Sean McAllister6525f662021-05-21 09:33:07 -0600940 AND build_end_time > '{1}'
941 AND build_end_time < '{2}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700942 )
943 SELECT
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700944 b.stage_name,
945 b.board,
946 b.milestone,
947 b.platform,
948 b.build_config,
949 FROM
950 relaxed_builds AS b
951 """