blob: 4d0b1a3e33e321431dc1ca0c6807a339d576a694 [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
Xixuan Wu5d6063e2017-09-05 16:15:07 -070062 # Explicitly specify timeout for http to avoid DeadlineExceededError.
63 # It's used for services like AndroidBuild API, which raise such error
64 # when being triggered too many calls in a short time frame.
65 # http://stackoverflow.com/questions/14698119/httpexception-deadline-exceeded-while-waiting-for-http-response-from-url-dead
Tim Baina602d462022-05-13 21:08:56 +000066 http_auth = self._credentials.authorize(
67 httplib2.Http(timeout=http_timeout_seconds))
Xixuan Wu5d6063e2017-09-05 16:15:07 -070068 if discovery_url is None:
69 self._service = apiclient.discovery.build(
70 self.service_name, self.service_version,
71 http=http_auth)
72 else:
73 self._service = apiclient.discovery.build(
74 self.service_name, self.service_version, http=http_auth,
75 discoveryServiceUrl=discovery_url)
xixuan878b1eb2017-03-20 15:58:17 -070076
Xixuan Wu5d6063e2017-09-05 16:15:07 -070077 def _init_credentials(self):
78 """Initialize the credentials for a google API."""
79 if (self.running_env == constants.RunningEnv.ENV_STANDALONE or
80 self.running_env == constants.RunningEnv.ENV_DEVELOPMENT_SERVER):
81 # Running locally
82 service_credentials = service_account.ServiceAccountCredentials
83 self._credentials = service_credentials.from_json_keyfile_name(
Xixuan Wu26d06e02017-09-20 14:50:28 -070084 file_getter.STAGING_CLIENT_SECRETS_FILE, self.scopes)
Xixuan Wu5d6063e2017-09-05 16:15:07 -070085 else:
86 # Running in app-engine production
87 self._credentials = appengine.AppAssertionCredentials(self.scopes)
xixuan878b1eb2017-03-20 15:58:17 -070088
89
90class AndroidBuildRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070091 """REST client for android build API."""
xixuan878b1eb2017-03-20 15:58:17 -070092
Xixuan Wu5d6063e2017-09-05 16:15:07 -070093 def __init__(self, rest_client):
94 """Initialize a REST client for connecting to Android Build API."""
95 self._rest_client = rest_client
96 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -070097
Xixuan Wu5d6063e2017-09-05 16:15:07 -070098 def get_latest_build_id(self, branch, target):
99 """Get the latest build id for a given branch and target.
xixuan878b1eb2017-03-20 15:58:17 -0700100
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700101 Args:
102 branch: an android build's branch
103 target: an android build's target
xixuan878b1eb2017-03-20 15:58:17 -0700104
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700105 Returns:
106 A string representing latest build id.
107 """
108 request = self._rest_client.service.build().list(
109 buildType='submitted',
110 branch=branch,
111 target=target,
112 successful=True,
113 maxResults=1)
114 builds = request.execute(num_retries=10)
115 if not builds or not builds['builds']:
116 return None
xixuan878b1eb2017-03-20 15:58:17 -0700117
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700118 return builds['builds'][0]['buildId']
xixuan878b1eb2017-03-20 15:58:17 -0700119
120
121class StorageRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700122 """REST client for google storage API."""
xixuan878b1eb2017-03-20 15:58:17 -0700123
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700124 def __init__(self, rest_client):
125 """Initialize a REST client for connecting to Google storage API."""
126 self._rest_client = rest_client
127 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -0700128
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700129 def read_object(self, bucket, object_path):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700130 """Read the contents of input_object in input_bucket.
xixuan878b1eb2017-03-20 15:58:17 -0700131
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700132 Args:
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700133 bucket: A string to indicate the bucket for fetching the object.
134 e.g. constants.StorageBucket.PROD_SUITE_SCHEDULER
135 object_path: A string to indicate the path of the object to read the
136 contents.
xixuan878b1eb2017-03-20 15:58:17 -0700137
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700138 Returns:
139 the stripped string contents of the input object.
xixuan878b1eb2017-03-20 15:58:17 -0700140
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700141 Raises:
142 apiclient.errors.HttpError
143 """
144 req = self._rest_client.service.objects().get_media(
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700145 bucket=bucket,
146 object=object_path)
147 return req.execute()
148
149 def upload_object(self, bucket, src_object_path, dest_object_path):
150 """Upload object_path to input_bucket.
151
152 Args:
153 bucket: A string to indicate the bucket for the object to be uploaded to.
154 src_object_path: A string the full path of the object to upload.
155 dest_object_path: A string path inside bucket to upload to.
156
157 Returns:
158 A dict of uploaded object info.
159
160 Raises:
161 apiclient.errors.HttpError
162 """
163 req = self._rest_client.service.objects().insert(
164 bucket=bucket,
165 name=dest_object_path,
166 media_body=src_object_path,
167 media_mime_type='text/plain',
168 )
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700169 return req.execute()
xixuan878b1eb2017-03-20 15:58:17 -0700170
171
172class CalendarRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700173 """Class of REST client for google calendar API."""
xixuan878b1eb2017-03-20 15:58:17 -0700174
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700175 def __init__(self, rest_client):
176 """Initialize a REST client for connecting to Google calendar API."""
177 self._rest_client = rest_client
178 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -0700179
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700180 def add_event(self, calendar_id, input_event):
181 """Add events of a given calendar.
xixuan878b1eb2017-03-20 15:58:17 -0700182
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700183 Args:
184 calendar_id: the ID of the given calendar.
185 input_event: the event to be added.
186 """
187 self._rest_client.service.events().insert(
188 calendarId=calendar_id,
189 body=input_event).execute()
xixuan878b1eb2017-03-20 15:58:17 -0700190
191
Xixuan Wu6f117e92017-10-27 10:51:58 -0700192class StackdriverRestClient(object):
193 """REST client for google storage API."""
194
195 def __init__(self, rest_client):
196 """Initialize a REST client for connecting to Google storage API."""
197 self._rest_client = rest_client
198 self._rest_client.create_service()
199
200 def read_logs(self, request):
Xinan Lin318cf752019-07-19 14:50:23 -0700201 # project_id, page_size, order_by, query_filter=''):
Xixuan Wu6f117e92017-10-27 10:51:58 -0700202 """Read the logs of the project_id based on all filters.
203
204 Args:
205 request: a request dict generated by
206 stackdriver_lib.form_logging_client_request.
207
208 Returns:
209 A json object, can be parsed by
210 stackdriver_lib.parse_logging_client_response.
211
212 Raises:
213 apiclient.errors.HttpError
214 """
215 req = self._rest_client.service.entries().list(
216 fields='entries/protoPayload', body=request)
217 return req.execute()
218
219
xixuan878b1eb2017-03-20 15:58:17 -0700220class SwarmingRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700221 """REST client for swarming proxy API."""
xixuan878b1eb2017-03-20 15:58:17 -0700222
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700223 DISCOVERY_URL_PATTERN = '%s/discovery/v1/apis/%s/%s/rest'
xixuan878b1eb2017-03-20 15:58:17 -0700224
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700225 def __init__(self, rest_client, service_url):
226 self._rest_client = rest_client
227 discovery_url = self.DISCOVERY_URL_PATTERN % (
228 service_url, rest_client.service_name, rest_client.service_version)
229 self._rest_client.create_service(discovery_url=discovery_url)
xixuan878b1eb2017-03-20 15:58:17 -0700230
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700231 def create_task(self, request):
232 """Create new task.
xixuan878b1eb2017-03-20 15:58:17 -0700233
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700234 Args:
235 request: a json-compatible dict expected by swarming server.
236 See _to_raw_request's output in swarming_lib.py for details.
xixuan878b1eb2017-03-20 15:58:17 -0700237
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700238 Returns:
239 A json dict returned by API task.new.
240 """
241 return self._rest_client.service.tasks().new(
242 fields='request,task_id', body=request).execute()
xixuan878b1eb2017-03-20 15:58:17 -0700243
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700244 def get_task_result(self, task_id):
245 """Get task results by a given task_id.
xixuan878b1eb2017-03-20 15:58:17 -0700246
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700247 Args:
248 task_id: A string, represents task id.
249
250 Returns:
251 A json dict returned by API task.result.
252 """
253 return self._rest_client.service.task().result(
254 task_id=task_id).execute()
Xixuan Wu7d142a92019-04-26 12:03:02 -0700255
256
257class BigqueryRestClient(object):
258 """Class of REST client for Bigquery API."""
259
Xixuan Wu55d38c52019-05-21 14:26:23 -0700260 PROJECT_TO_RUN_BIGQUERY_JOB = 'google.com:suite-scheduler'
Tim Baina602d462022-05-13 21:08:56 +0000261 QUERY_TIMEOUT_SECONDS = 200
Xixuan Wu55d38c52019-05-21 14:26:23 -0700262
Xinan Linc9f01152020-02-05 22:05:13 -0800263 def __init__(self, rest_client, project=None, dataset=None, table=None):
Xixuan Wu7d142a92019-04-26 12:03:02 -0700264 """Initialize a REST client for connecting to Bigquery API."""
265 self._rest_client = rest_client
Tim Baina602d462022-05-13 21:08:56 +0000266 # We always want to use an HTTP timeout that's greater than the
267 # underlying query timeout.
268 self._rest_client.create_service(
269 http_timeout_seconds=self.QUERY_TIMEOUT_SECONDS + 5)
Xinan Linc9f01152020-02-05 22:05:13 -0800270 self.project = project
271 self.dataset = dataset
272 self.table = table
Xixuan Wu7d142a92019-04-26 12:03:02 -0700273
Xixuan Wu55d38c52019-05-21 14:26:23 -0700274 def query(self, query_str):
275 """Query bigquery.
Xixuan Wu7d142a92019-04-26 12:03:02 -0700276
277 Args:
Xixuan Wu55d38c52019-05-21 14:26:23 -0700278 query_str: A string used to query Bigquery.
Xixuan Wu7d142a92019-04-26 12:03:02 -0700279
280 Returns:
281 A json dict returned by API bigquery.jobs.query, e.g.
282 # {...,
283 # "rows": [
284 # {
285 # "f": [ # field
286 # {
287 # "v": # value
Xixuan Wu55d38c52019-05-21 14:26:23 -0700288 # },
289 # {
290 # "v": # value
291 # },
292 # ...
Xixuan Wu7d142a92019-04-26 12:03:02 -0700293 # ]
294 # }
Xixuan Wuf856ff12019-05-21 14:09:38 -0700295 # ...
Xixuan Wu7d142a92019-04-26 12:03:02 -0700296 # ]
297 # }
298 """
Tim Baina602d462022-05-13 21:08:56 +0000299 query_timeout_ms = self.QUERY_TIMEOUT_SECONDS * 1000
Xixuan Wu7d142a92019-04-26 12:03:02 -0700300 query_data = {
Xixuan Wu55d38c52019-05-21 14:26:23 -0700301 'query': query_str,
302 'useLegacySql': False,
Tim Baina602d462022-05-13 21:08:56 +0000303 'timeoutMs': query_timeout_ms,
Xixuan Wu7d142a92019-04-26 12:03:02 -0700304 }
Sean McAllister7d021782021-07-15 08:59:57 -0600305
306 for cnt in range(RETRY_LIMIT+1):
307 try:
308 return self._rest_client.service.jobs().query(
309 projectId=self.PROJECT_TO_RUN_BIGQUERY_JOB,
310 fields='rows',
311 body=query_data).execute()
312 except apiclient.errors.HttpError as ex:
313 status = ex.resp.status
314 if status in [500, 502, 503, 504]:
315 if cnt < RETRY_LIMIT:
316 logging.warning("Got response status %d, retrying" % status)
317 time.sleep(5)
318 else:
319 logging.error(
320 "Retry limit of %d hit communicating with BigQuery" % RETRY_LIMIT)
321 raise
322
Xixuan Wu55d38c52019-05-21 14:26:23 -0700323
Xinan Linc9f01152020-02-05 22:05:13 -0800324 def insert(self, rows):
325 """Insert rows to specified Bigquery table.
326
327 Args:
328 rows: list of json objects.
329
330 Raise:
331 RestClientError: if project/dataset/table is not defined.
332 """
333 if not any([self.project, self.dataset, self.table]):
334 raise RestClientError('Project, dataset, table should be all set.'
335 'Got project:%s, dataset:%s, table:%s' %
336 (self.project, self.dataset, self.table))
337 body = {
338 'kind': 'bigquery#tableDataInsertAllRequest',
339 'rows': rows,
340 }
341 request = self._rest_client.service.tabledata().insertAll(
342 projectId=self.project,
343 datasetId=self.dataset,
344 tableId=self.table,
345 body=body)
346 response = request.execute(num_retries=3)
347 if response.get('insertErrors'):
348 logging.error('InsertRequest reported errors: %r',
349 response.get('insertErrors'))
350 return False
351
352 return True
Xixuan Wu55d38c52019-05-21 14:26:23 -0700353
Xinan Lin80a9d932019-10-17 09:24:43 -0700354class CrOSTestPlatformBigqueryClient(BigqueryRestClient):
355 """REST client for cros_test_platform builder Bigquery API."""
Xixuan Wu55d38c52019-05-21 14:26:23 -0700356
Xinan Lin80a9d932019-10-17 09:24:43 -0700357 def get_past_job_nums(self, hours):
358 """Query the count of the jobs kicked off to cros_test_platform.
Xixuan Wu55d38c52019-05-21 14:26:23 -0700359
360 Args:
361 hours: An integer.
362
363 Returns:
364 An integer.
365 """
366 query_str = """
367 SELECT
368 COUNT(*)
369 FROM
Xinan Lin80a9d932019-10-17 09:24:43 -0700370 `cr-buildbucket.chromeos.builds`
Xixuan Wu55d38c52019-05-21 14:26:23 -0700371 WHERE
Xinan Lin80a9d932019-10-17 09:24:43 -0700372 created_by = 'user:suite-scheduler.google.com@appspot.gserviceaccount.com'
373 and create_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL %d HOUR);
Xixuan Wu55d38c52019-05-21 14:26:23 -0700374 """
375 res = self.query(query_str % hours)
376 try:
377 return int(_parse_bq_job_query(res)[0][0])
378 except (ValueError, KeyError) as e:
379 logging.debug('The returned json: \n%r', res)
380 logging.exception(str(e))
381 raise
382
383
Xixuan Wuf856ff12019-05-21 14:09:38 -0700384class BuildBucketBigqueryClient(BigqueryRestClient):
385 """Rest client for buildbucket Bigquery API."""
386
Xinan Lin028f9582019-12-11 10:55:33 -0800387 def get_latest_passed_firmware_builds(self):
388 """Get artifact link of the latest passed firmware builds for board.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700389
Xinan Lin028f9582019-12-11 10:55:33 -0800390 The query returns the latest firmware build for the combination of
391 board and build spec, which is cros or firmware. No restriction set
392 in the query, so it should return all available builds.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700393
394 Returns:
Xinan Lin028f9582019-12-11 10:55:33 -0800395 A list of (spec, board, firmware_artifact_link).
Xixuan Wuf856ff12019-05-21 14:09:38 -0700396 """
397 query_str = """
398 SELECT
Xinan Lin028f9582019-12-11 10:55:33 -0800399 spec,
400 board,
401 /*
402 * Firmware builds may contain artifacts for multiple boards in a
403 * single build - each in a separate directory.
404 */
405 IF(spec = 'firmware', CONCAT(artifact, '/', board), artifact) as artifact
Xixuan Wuf856ff12019-05-21 14:09:38 -0700406 FROM
Xinan Lin028f9582019-12-11 10:55:33 -0800407 (
408 SELECT
409 spec,
410 board,
411 artifact,
412 RANK() OVER (PARTITION BY spec, board ORDER BY end_time DESC) AS rank
413 FROM
414 (
415 SELECT
416 /*
417 * build_config is a string contains the board and build type.
418 * For Cros build, it has the form of "BoardName-release", while
419 * the firmware config shows like "firmware-BoardName-[1]-firmwarebranch".
420 * [1] is the firmware ver.
421 */
422 IF(prefix = 'firmware', 'firmware', 'cros') AS spec,
Jeremy Bettisd299c732022-02-23 12:41:03 -0700423 IF(prefix = 'firmware', COALESCE(
424 SPLIT(firmware_tarball, '/') [OFFSET(0)],
425 SPLIT(build_config, '-') [OFFSET(1)]
426 ), cros_prefix) AS board,
Xinan Lin028f9582019-12-11 10:55:33 -0800427 artifact,
428 end_time
429 FROM
430 (
431 SELECT
432 SPLIT(build_config, '-') [OFFSET(0)] AS prefix,
Jeremy Bettisd299c732022-02-23 12:41:03 -0700433 build_config,
Xinan Lin028f9582019-12-11 10:55:33 -0800434 REGEXP_EXTRACT(
435 build_config, r"(^[a-zA-Z0-9_.+-]+)-release"
436 ) as cros_prefix,
437 end_time,
Jeremy Bettisd299c732022-02-23 12:41:03 -0700438 artifact,
439 JSON_VALUE(fbb, '$') as firmware_tarball
Xinan Lin028f9582019-12-11 10:55:33 -0800440 FROM
441 (
442 SELECT
Sean McAllister53dd3d82021-05-18 15:15:14 -0600443 COALESCE(
444 JSON_EXTRACT_SCALAR(
445 output.properties, '$.artifact_link'
446 ),
447 FORMAT('gs://%s/%s',
448 JSON_EXTRACT_SCALAR(output.properties, '$.artifacts.gs_bucket'),
449 JSON_EXTRACT_SCALAR(output.properties, '$.artifacts.gs_path'))
Sean McAllister3fb4cec2021-04-20 22:38:38 +0000450 ) as artifact,
Sean McAllister53dd3d82021-05-18 15:15:14 -0600451 COALESCE(
452 JSON_EXTRACT_SCALAR(
453 output.properties, '$.cbb_config'
454 ),
455 builder.builder
Xinan Lin028f9582019-12-11 10:55:33 -0800456 ) as build_config,
Jeremy Bettisd299c732022-02-23 12:41:03 -0700457 end_time,
458 JSON_EXTRACT_ARRAY(output.properties, '$.artifacts.files_by_artifact.FIRMWARE_TARBALL') as firmware_by_board
Xinan Lin028f9582019-12-11 10:55:33 -0800459 FROM `cr-buildbucket.chromeos.completed_builds_BETA`
460 WHERE
461 status = 'SUCCESS'
462 AND JSON_EXTRACT_SCALAR(
463 output.properties, '$.suite_scheduling'
464 ) = 'True'
Jeremy Bettisd299c732022-02-23 12:41:03 -0700465 ) LEFT JOIN UNNEST(firmware_by_board) as fbb
Xinan Lin028f9582019-12-11 10:55:33 -0800466 )
467 )
468 )
469 WHERE rank = 1
Xixuan Wuf856ff12019-05-21 14:09:38 -0700470 """
Xinan Lin028f9582019-12-11 10:55:33 -0800471 res = self.query(query_str)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700472 res = _parse_bq_job_query(res)
473 if res is None:
474 return None
Xinan Lin028f9582019-12-11 10:55:33 -0800475 logging.info('Fetched the latest artifact links: %s',
476 [row[2] for row in res])
477 return res
Xinan Lin318cf752019-07-19 14:50:23 -0700478
Jack Neus8f0edb42022-03-17 20:21:39 +0000479 # TODO(b/225382624): Remove this when Rubik is the only source of builds.
Xinan Lin71eeeb02020-03-10 17:37:12 -0700480 def get_passed_builds(self, earliest_end_time, latest_end_time, event_type):
Xinan Linea1efcb2019-12-30 23:46:42 -0800481 """Get passed builds inside a given time span.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700482
Xinan Lin3330d672020-03-03 14:52:36 -0800483 BigQuery does not guarantee the inserted time of rows. A new build
484 may not get inserted when suite scheduler runs the query. To avoid
Xinan Lin71eeeb02020-03-10 17:37:12 -0700485 it, we scan each time span twice:
Xinan Lin3330d672020-03-03 14:52:36 -0800486 - the first run catches the new build from earliest_end_time to
487 latest_end_time, and inserts the result to a temp BQ table.
488 - the second run checks the build from (earliest_end_time - 1Day)
489 to (latest_end_time - 1Day) plus (earliest_end_time to
490 latest_end_time). The query returns the build which does not
491 appear in the temp table. Thus, if a build was not fetched by the
Xinan Lin71eeeb02020-03-10 17:37:12 -0700492 first run, we still could schedule test on it at most 1 day later
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700493 for new_build events.
494 Weekly and nightly events do not need this arrangement because
495 they do not cover every single build.
496
Xinan Lin3330d672020-03-03 14:52:36 -0800497
Xixuan Wuf856ff12019-05-21 14:09:38 -0700498 Args:
Xinan Linea1efcb2019-12-30 23:46:42 -0800499 earliest_end_time: a datetime.datetime object in UTC.
500 latest_end_time: a datetime.datetime object in UTC.
Xinan Lin71eeeb02020-03-10 17:37:12 -0700501 event_type: a string of event type. It could be one of
502 [WEEKLY|NIGHTLY|new_build].
Xixuan Wuf856ff12019-05-21 14:09:38 -0700503
504 Returns:
505 A list of build_lib.BuildInfo objects.
506 """
Xinan Lin761b0c52020-03-25 17:31:57 -0700507 base_query_template = """
Sean McAllister53dd3d82021-05-18 15:15:14 -0600508 WITH builds AS
509 (SELECT
510 COALESCE(
511 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
512 JSON_EXTRACT_SCALAR(output.properties, '$.build_targets[0].name')
513 ) AS board,
514 COALESCE(
515 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
516 REPLACE(
517 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(0)], 'R', '')
518 ) AS milestone,
519 COALESCE(
520 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version'),
521 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(1)]
522 ) AS platform,
523 COALESCE(
524 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config'),
525 builder.builder
526 ) AS build_config,
527
528 -- Time info
529 end_time as build_end_time,
530 CURRENT_TIMESTAMP() as inserted_time,
531 FROM `cr-buildbucket.chromeos.builds`
532 WHERE
533 status = 'SUCCESS'
534 AND JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True')
535
536 SELECT
537 *,
538 '{0}' as event_type
539 FROM builds
540 WHERE
541 board IS NOT NULL
Xixuan Wuf856ff12019-05-21 14:09:38 -0700542 """
Jack Neus8f0edb42022-03-17 20:21:39 +0000543 return self._get_passed_builds(base_query_template, earliest_end_time, latest_end_time, event_type)
544
545 def get_passed_rubik_builds(self, earliest_end_time, latest_end_time, event_type):
546 """Get passed Rubik builds inside a given time span.
547
548 BigQuery does not guarantee the inserted time of rows. A new build
549 may not get inserted when suite scheduler runs the query. To avoid
550 it, we scan each time span twice:
551 - the first run catches the new build from earliest_end_time to
552 latest_end_time, and inserts the result to a temp BQ table.
553 - the second run checks the build from (earliest_end_time - 1Day)
554 to (latest_end_time - 1Day) plus (earliest_end_time to
555 latest_end_time). The query returns the build which does not
556 appear in the temp table. Thus, if a build was not fetched by the
557 first run, we still could schedule test on it at most 1 day later
558 for new_build events.
559 Weekly and nightly events do not need this arrangement because
560 they do not cover every single build.
561
562
563 Args:
564 earliest_end_time: a datetime.datetime object in UTC.
565 latest_end_time: a datetime.datetime object in UTC.
566 event_type: a string of event type. It could be one of
567 [WEEKLY|NIGHTLY|new_build].
568
569 Returns:
570 A list of build_lib.BuildInfo objects.
571 """
572 base_query_template = """
573 WITH builds AS (
574 SELECT
575 JSON_EXTRACT_SCALAR(input.properties, '$.build_target.name') AS board,
Jack Neus6c270dd2022-03-18 20:02:48 +0000576 JSON_EXTRACT_SCALAR(output.properties, '$.target_versions.milestoneVersion') AS milestone,
Jack Neus8f0edb42022-03-17 20:21:39 +0000577 JSON_EXTRACT_SCALAR(output.properties, '$.target_versions.platformVersion') AS platform,
Jack Neus447653b2022-03-23 18:07:46 +0000578 CONCAT(JSON_EXTRACT_SCALAR(input.properties, '$.build_target.name'), "-release") AS build_config,
Jack Neus8f0edb42022-03-17 20:21:39 +0000579 -- Time info
580 end_time as build_end_time,
581 CURRENT_TIMESTAMP() as inserted_time,
582 FROM `cr-buildbucket.chromeos.builds`
583 WHERE
584 status = 'SUCCESS' AND
Jack Neus50603cf2022-03-18 20:30:48 +0000585 JSON_EXTRACT_SCALAR(input.properties, '$.recipe') = 'build_release' AND
586 builder.builder NOT LIKE "staging-%"
Jack Neus8f0edb42022-03-17 20:21:39 +0000587 )
588 SELECT
589 *,
590 '{0}' as event_type
591 FROM builds
592 WHERE
593 board IS NOT NULL
594 """
595 return self._get_passed_builds(base_query_template, earliest_end_time, latest_end_time, event_type)
596
597 def _get_passed_builds(self, base_query_template, earliest_end_time, latest_end_time, event_type):
598 """Get passed builds inside a given time span.
599
600 BigQuery does not guarantee the inserted time of rows. A new build
601 may not get inserted when suite scheduler runs the query. To avoid
602 it, we scan each time span twice:
603 - the first run catches the new build from earliest_end_time to
604 latest_end_time, and inserts the result to a temp BQ table.
605 - the second run checks the build from (earliest_end_time - 1Day)
606 to (latest_end_time - 1Day) plus (earliest_end_time to
607 latest_end_time). The query returns the build which does not
608 appear in the temp table. Thus, if a build was not fetched by the
609 first run, we still could schedule test on it at most 1 day later
610 for new_build events.
611 Weekly and nightly events do not need this arrangement because
612 they do not cover every single build.
613
614
615 Args:
616 base_query_template: base query to use to find release builds.
617 earliest_end_time: a datetime.datetime object in UTC.
618 latest_end_time: a datetime.datetime object in UTC.
619 event_type: a string of event type. It could be one of
620 [WEEKLY|NIGHTLY|new_build].
621
622 Returns:
623 A list of build_lib.BuildInfo objects.
624 """
Xinan Lin761b0c52020-03-25 17:31:57 -0700625 base_query_str = base_query_template.format(event_type)
Xinan Lin71eeeb02020-03-10 17:37:12 -0700626 earliest_end_time_str = earliest_end_time.strftime(
627 time_converter.TIME_FORMAT)
628 latest_end_time_str = latest_end_time.strftime(time_converter.TIME_FORMAT)
629 project_id = constants.AppID.STAGING_APP
630 if constants.environment() == constants.RunningEnv.ENV_PROD:
631 project_id = constants.application_id()
Xinan Lin3330d672020-03-03 14:52:36 -0800632
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700633 if event_type == 'new_build':
634 insert_passed_builds = """
635 INSERT
636 `google.com:{0}.builds.passed_builds`(
637 board,
638 milestone,
639 platform,
640 build_config,
641 build_end_time,
642 inserted_time,
643 event_type
644 ) {1}
Sean McAllister909997a2021-05-19 13:28:25 -0600645 AND build_end_time > '{2}'
646 AND build_end_time < '{3}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700647 """
Sean McAllister909997a2021-05-19 13:28:25 -0600648
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700649 # Insert the currently visible builds to BQ.
650 logging.info(
651 'Insert the visible passed builds '
652 'between %s and %s to BQ.', earliest_end_time_str,
653 latest_end_time_str)
Sean McAllister909997a2021-05-19 13:28:25 -0600654
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700655 self.query(
656 insert_passed_builds.format(project_id, base_query_str,
657 earliest_end_time_str,
658 latest_end_time_str))
Xixuan Wuf856ff12019-05-21 14:09:38 -0700659
Sean McAllister53dd3d82021-05-18 15:15:14 -0600660 query_template = _passed_build_query_template(event_type)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700661 query_str = query_template.format(base_query_str, earliest_end_time_str,
662 latest_end_time_str, project_id)
Xinan Lin3330d672020-03-03 14:52:36 -0800663 if global_config.GAE_TESTING:
Xinan Lin71eeeb02020-03-10 17:37:12 -0700664 query_str += 'LIMIT 10'
665 logging.info('Getting passed builds finished between %s and %s',
666 earliest_end_time_str, latest_end_time_str)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700667 res = self.query(query_str)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700668 res = _parse_bq_job_query(res)
669 if res is None:
670 return []
671
672 build_infos = []
673 for board, milestone, platform, build_config in res:
Xixuan Wuf856ff12019-05-21 14:09:38 -0700674 build_infos.append(
675 build_lib.BuildInfo(board, None, milestone, platform, build_config))
676
677 return build_infos
678
Xinan Lin71eeeb02020-03-10 17:37:12 -0700679 def get_relaxed_passed_builds(self, earliest_end_time, latest_end_time,
680 event_type):
Jared Loucksa676b5d2022-04-15 15:18:44 -0600681 """Get builds with successful UploadTestArtifacts stages in a given span.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700682
Xinan Lin71eeeb02020-03-10 17:37:12 -0700683 Same as get_passed_builds, we run the query twice to ensure we
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700684 fetched all builds from BQ for new_build event.
Xinan Lin3330d672020-03-03 14:52:36 -0800685
Xixuan Wuf856ff12019-05-21 14:09:38 -0700686 Args:
Xinan Linea1efcb2019-12-30 23:46:42 -0800687 earliest_end_time: a datetime.datetime object in UTC.
688 latest_end_time: a datetime.datetime object in UTC.
Xinan Lin71eeeb02020-03-10 17:37:12 -0700689 event_type: a string of event type.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700690
691 Returns:
692 A list of build_lib.BuildInfo objects.
693 """
Xinan Lin761b0c52020-03-25 17:31:57 -0700694 base_query_template = """
Sean McAllister53dd3d82021-05-18 15:15:14 -0600695 WITH builds AS
696 (SELECT
697 COALESCE(
698 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
699 JSON_EXTRACT_SCALAR(output.properties, '$.build_targets[0].name')
700 ) AS board,
701 COALESCE(
702 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
703 REPLACE(
704 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(0)], 'R', '')
705 ) AS milestone,
706 COALESCE(
707 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version'),
708 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(1)]
709 ) AS platform,
710 COALESCE(
711 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config'),
712 builder.builder
713 ) AS build_config,
714
715 step.name AS stage_name,
716
717 -- Time info
718 build.end_time as build_end_time,
719 CURRENT_TIMESTAMP() as inserted_time,
720 FROM `cr-buildbucket.chromeos.builds` build,
721 UNNEST(build.steps) AS step
722 WHERE
723 build.status != 'SUCCESS'
Jared Loucksa676b5d2022-04-15 15:18:44 -0600724 AND step.name = 'UploadTestArtifacts'
Sean McAllister53dd3d82021-05-18 15:15:14 -0600725 AND step.status = 'SUCCESS'
726 AND JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True')
727
728 SELECT
729 *,
730 '{0}' AS event_type
731 FROM
732 builds
733 WHERE board IS NOT NULL
Xixuan Wuf856ff12019-05-21 14:09:38 -0700734 """
Sean McAllister53dd3d82021-05-18 15:15:14 -0600735
Xinan Lin761b0c52020-03-25 17:31:57 -0700736 base_query_str = base_query_template.format(event_type)
Xinan Lin71eeeb02020-03-10 17:37:12 -0700737 earliest_end_time_str = earliest_end_time.strftime(
738 time_converter.TIME_FORMAT)
739 latest_end_time_str = latest_end_time.strftime(time_converter.TIME_FORMAT)
740 project_id = constants.AppID.STAGING_APP
741 if constants.environment() == constants.RunningEnv.ENV_PROD:
742 project_id = constants.application_id()
Xinan Lin3330d672020-03-03 14:52:36 -0800743
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700744 if event_type == 'new_build':
745 insert_relaxed_builds = """
746 INSERT
747 `google.com:{0}.builds.relaxed_builds`(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700748 stage_name,
749 board,
750 milestone,
751 platform,
752 build_config,
753 build_end_time,
754 inserted_time,
755 event_type
756 ) {1}
Sean McAllister909997a2021-05-19 13:28:25 -0600757 AND build_end_time > '{2}'
758 AND build_end_time < '{3}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700759 """
760 logging.info(
761 'Insert the visible relaxed builds '
762 'between %s and %s to BQ.', earliest_end_time_str,
763 latest_end_time_str)
Sean McAllister909997a2021-05-19 13:28:25 -0600764
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700765 self.query(
766 insert_relaxed_builds.format(project_id, base_query_str,
767 earliest_end_time_str,
768 latest_end_time_str))
Xixuan Wuf856ff12019-05-21 14:09:38 -0700769
Sean McAllister53dd3d82021-05-18 15:15:14 -0600770 query_template = _relaxed_build_query_template(event_type)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700771 query_str = query_template.format(base_query_str, earliest_end_time_str,
772 latest_end_time_str, project_id)
Xinan Lin3330d672020-03-03 14:52:36 -0800773 if global_config.GAE_TESTING:
Xinan Lin71eeeb02020-03-10 17:37:12 -0700774 query_str += 'LIMIT 10'
775 logging.info('Getting relaxed passed builds finished between %s and %s',
776 earliest_end_time_str, latest_end_time_str)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700777 res = self.query(query_str)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700778 res = _parse_bq_job_query(res)
779 if res is None:
780 return []
781
782 build_infos = []
Sean McAllister53dd3d82021-05-18 15:15:14 -0600783 for stage_name, board, milestone, platform, build_config in res:
Xixuan Wuf856ff12019-05-21 14:09:38 -0700784 build_infos.append(
Jared Loucksa676b5d2022-04-15 15:18:44 -0600785 build_lib.BuildInfo(board, None, milestone, platform, build_config))
Xixuan Wuf856ff12019-05-21 14:09:38 -0700786
787 return build_infos
788
789
Xixuan Wu55d38c52019-05-21 14:26:23 -0700790def _parse_bq_job_query(json_input):
791 """Parse response from API bigquery.jobs.query.
792
793 Args:
794 json_input: a dict, representing jsons returned by query API.
795
796 Returns:
797 A 2D string matrix: [rows[columns]], or None if no result.
798 E.g. Input:
799 "rows": [
800 {
801 "f": [ # field
802 {
803 "v": 'foo1',
804 },
805 {
806 "v": 'foo2',
807 }
808 ]
809 }
810 {
811 "f": [ # field
812 {
813 "v": 'bar1',
814 },
815 {
816 "v": 'bar2',
817 }
818 ]
819 }
820 ]
821 => Output: [['foo1', 'foo2'], ['bar1', 'bar2']]
822 """
823 if 'rows' not in json_input:
824 return None
825
826 res = []
827 for r in json_input['rows']:
828 rc = []
829 for c in r['f']:
830 rc.append(c['v'])
831
832 res.append(rc)
833
834 return res
Xixuan Wuf856ff12019-05-21 14:09:38 -0700835
Sean McAllister53dd3d82021-05-18 15:15:14 -0600836def _passed_build_query_template(event_type):
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700837 """Wrapper to create the query template for passed builds."""
838 if event_type == 'new_build':
839 return """
840 WITH passed_builds AS
841 (
842 {0}
843 AND (
Sean McAllister6525f662021-05-21 09:33:07 -0600844 (build_end_time > TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700845 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'), INTERVAL 1 DAY)
Sean McAllister6525f662021-05-21 09:33:07 -0600846 AND build_end_time < TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700847 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'), INTERVAL 1 DAY))
Sean McAllister6525f662021-05-21 09:33:07 -0600848 OR (build_end_time > '{1}' AND build_end_time < '{2}')
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700849 )
850 )
851 SELECT
852 b.board,
853 b.milestone,
854 b.platform,
855 b.build_config,
856 FROM
857 passed_builds AS b
858 LEFT JOIN
859 `google.com:{3}.builds.passed_builds` AS r
860 ON (
861 r.board = b.board
862 AND r.milestone = b.milestone
863 AND r.build_config = b.build_config
864 AND r.platform = b.platform
865 AND r.event_type = b.event_type
866 AND r.build_end_time > TIMESTAMP_SUB(
867 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'),
868 INTERVAL 1 DAY)
869 AND r.build_end_time < TIMESTAMP_SUB(
870 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'),
871 INTERVAL 1 DAY)
872 )
873 WHERE
874 r.inserted_time is null
875 """
876 return """
877 WITH passed_builds AS
878 (
879 {0}
Sean McAllister6525f662021-05-21 09:33:07 -0600880 AND build_end_time > '{1}'
881 AND build_end_time < '{2}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700882 )
883 SELECT
884 b.board,
885 b.milestone,
886 b.platform,
887 b.build_config,
888 FROM
889 passed_builds AS b
890 """
891
892
Sean McAllister53dd3d82021-05-18 15:15:14 -0600893def _relaxed_build_query_template(event_type):
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700894 """Wrapper to create the query template for relaxed builds."""
895 if event_type == 'new_build':
896 return """
897 WITH relaxed_builds AS
898 (
899 {0}
900 AND (
Sean McAllister6525f662021-05-21 09:33:07 -0600901 (build_end_time > TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700902 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'), INTERVAL 1 DAY)
Sean McAllister6525f662021-05-21 09:33:07 -0600903 AND build_end_time < TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700904 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'), INTERVAL 1 DAY))
Sean McAllister6525f662021-05-21 09:33:07 -0600905 OR (build_end_time > '{1}' AND build_end_time < '{2}')
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700906 )
907 )
908 SELECT
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700909 b.stage_name,
910 b.board,
911 b.milestone,
912 b.platform,
913 b.build_config,
914 FROM
915 relaxed_builds AS b
916 LEFT JOIN
917 `google.com:{3}.builds.relaxed_builds` AS r
918 ON (
919 r.board = b.board
920 AND r.milestone = b.milestone
921 AND r.build_config = b.build_config
922 AND r.platform = b.platform
923 AND r.event_type = b.event_type
924 AND r.build_end_time > TIMESTAMP_SUB(
925 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'),
926 INTERVAL 1 DAY)
927 AND r.build_end_time < TIMESTAMP_SUB(
928 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'),
929 INTERVAL 1 DAY)
930 )
931 WHERE
932 r.inserted_time is null
933 """
934 return """
935 WITH relaxed_builds AS
936 (
937 {0}
Sean McAllister6525f662021-05-21 09:33:07 -0600938 AND build_end_time > '{1}'
939 AND build_end_time < '{2}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700940 )
941 SELECT
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700942 b.stage_name,
943 b.board,
944 b.milestone,
945 b.platform,
946 b.build_config,
947 FROM
948 relaxed_builds AS b
949 """