blob: 059e8e9a91293d80b1fe5e815102feec405ada9b [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
Julio Hurtadoa23da2a2022-08-23 17:53:34 +0000541 board IS NOT NULL AND
542 milestone != "None" AND
543 milestone IS NOT NULL
Xixuan Wuf856ff12019-05-21 14:09:38 -0700544 """
Julio Hurtadoa23da2a2022-08-23 17:53:34 +0000545 return self._get_passed_builds(base_query_template, earliest_end_time,
546 latest_end_time, event_type)
Jack Neus8f0edb42022-03-17 20:21:39 +0000547
548 def get_passed_rubik_builds(self, earliest_end_time, latest_end_time, event_type):
549 """Get passed Rubik builds inside a given time span.
550
551 BigQuery does not guarantee the inserted time of rows. A new build
552 may not get inserted when suite scheduler runs the query. To avoid
553 it, we scan each time span twice:
554 - the first run catches the new build from earliest_end_time to
555 latest_end_time, and inserts the result to a temp BQ table.
556 - the second run checks the build from (earliest_end_time - 1Day)
557 to (latest_end_time - 1Day) plus (earliest_end_time to
558 latest_end_time). The query returns the build which does not
559 appear in the temp table. Thus, if a build was not fetched by the
560 first run, we still could schedule test on it at most 1 day later
561 for new_build events.
562 Weekly and nightly events do not need this arrangement because
563 they do not cover every single build.
564
565
566 Args:
567 earliest_end_time: a datetime.datetime object in UTC.
568 latest_end_time: a datetime.datetime object in UTC.
569 event_type: a string of event type. It could be one of
570 [WEEKLY|NIGHTLY|new_build].
571
572 Returns:
573 A list of build_lib.BuildInfo objects.
574 """
575 base_query_template = """
576 WITH builds AS (
577 SELECT
578 JSON_EXTRACT_SCALAR(input.properties, '$.build_target.name') AS board,
Jack Neus6c270dd2022-03-18 20:02:48 +0000579 JSON_EXTRACT_SCALAR(output.properties, '$.target_versions.milestoneVersion') AS milestone,
Jack Neus8f0edb42022-03-17 20:21:39 +0000580 JSON_EXTRACT_SCALAR(output.properties, '$.target_versions.platformVersion') AS platform,
Jack Neus447653b2022-03-23 18:07:46 +0000581 CONCAT(JSON_EXTRACT_SCALAR(input.properties, '$.build_target.name'), "-release") AS build_config,
Jack Neus8f0edb42022-03-17 20:21:39 +0000582 -- Time info
583 end_time as build_end_time,
584 CURRENT_TIMESTAMP() as inserted_time,
585 FROM `cr-buildbucket.chromeos.builds`
586 WHERE
587 status = 'SUCCESS' AND
Jack Neus50603cf2022-03-18 20:30:48 +0000588 JSON_EXTRACT_SCALAR(input.properties, '$.recipe') = 'build_release' AND
589 builder.builder NOT LIKE "staging-%"
Jack Neus8f0edb42022-03-17 20:21:39 +0000590 )
591 SELECT
592 *,
593 '{0}' as event_type
594 FROM builds
595 WHERE
Julio Hurtadoa23da2a2022-08-23 17:53:34 +0000596 board IS NOT NULL AND
597 milestone != "None" AND
598 milestone IS NOT NULL
Jack Neus8f0edb42022-03-17 20:21:39 +0000599 """
600 return self._get_passed_builds(base_query_template, earliest_end_time, latest_end_time, event_type)
601
602 def _get_passed_builds(self, base_query_template, earliest_end_time, latest_end_time, event_type):
603 """Get passed builds inside a given time span.
604
605 BigQuery does not guarantee the inserted time of rows. A new build
606 may not get inserted when suite scheduler runs the query. To avoid
607 it, we scan each time span twice:
608 - the first run catches the new build from earliest_end_time to
609 latest_end_time, and inserts the result to a temp BQ table.
610 - the second run checks the build from (earliest_end_time - 1Day)
611 to (latest_end_time - 1Day) plus (earliest_end_time to
612 latest_end_time). The query returns the build which does not
613 appear in the temp table. Thus, if a build was not fetched by the
614 first run, we still could schedule test on it at most 1 day later
615 for new_build events.
616 Weekly and nightly events do not need this arrangement because
617 they do not cover every single build.
618
619
620 Args:
621 base_query_template: base query to use to find release builds.
622 earliest_end_time: a datetime.datetime object in UTC.
623 latest_end_time: a datetime.datetime object in UTC.
624 event_type: a string of event type. It could be one of
625 [WEEKLY|NIGHTLY|new_build].
626
627 Returns:
628 A list of build_lib.BuildInfo objects.
629 """
Xinan Lin761b0c52020-03-25 17:31:57 -0700630 base_query_str = base_query_template.format(event_type)
Xinan Lin71eeeb02020-03-10 17:37:12 -0700631 earliest_end_time_str = earliest_end_time.strftime(
632 time_converter.TIME_FORMAT)
633 latest_end_time_str = latest_end_time.strftime(time_converter.TIME_FORMAT)
634 project_id = constants.AppID.STAGING_APP
635 if constants.environment() == constants.RunningEnv.ENV_PROD:
636 project_id = constants.application_id()
Xinan Lin3330d672020-03-03 14:52:36 -0800637
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700638 if event_type == 'new_build':
639 insert_passed_builds = """
640 INSERT
641 `google.com:{0}.builds.passed_builds`(
642 board,
643 milestone,
644 platform,
645 build_config,
646 build_end_time,
647 inserted_time,
648 event_type
649 ) {1}
Sean McAllister909997a2021-05-19 13:28:25 -0600650 AND build_end_time > '{2}'
651 AND build_end_time < '{3}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700652 """
Sean McAllister909997a2021-05-19 13:28:25 -0600653
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700654 # Insert the currently visible builds to BQ.
655 logging.info(
656 'Insert the visible passed builds '
657 'between %s and %s to BQ.', earliest_end_time_str,
658 latest_end_time_str)
Sean McAllister909997a2021-05-19 13:28:25 -0600659
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700660 self.query(
661 insert_passed_builds.format(project_id, base_query_str,
662 earliest_end_time_str,
663 latest_end_time_str))
Xixuan Wuf856ff12019-05-21 14:09:38 -0700664
Sean McAllister53dd3d82021-05-18 15:15:14 -0600665 query_template = _passed_build_query_template(event_type)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700666 query_str = query_template.format(base_query_str, earliest_end_time_str,
667 latest_end_time_str, project_id)
Xinan Lin3330d672020-03-03 14:52:36 -0800668 if global_config.GAE_TESTING:
Xinan Lin71eeeb02020-03-10 17:37:12 -0700669 query_str += 'LIMIT 10'
670 logging.info('Getting passed builds finished between %s and %s',
671 earliest_end_time_str, latest_end_time_str)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700672 res = self.query(query_str)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700673 res = _parse_bq_job_query(res)
674 if res is None:
675 return []
676
677 build_infos = []
678 for board, milestone, platform, build_config in res:
Xixuan Wuf856ff12019-05-21 14:09:38 -0700679 build_infos.append(
680 build_lib.BuildInfo(board, None, milestone, platform, build_config))
681
682 return build_infos
683
Xinan Lin71eeeb02020-03-10 17:37:12 -0700684 def get_relaxed_passed_builds(self, earliest_end_time, latest_end_time,
685 event_type):
Jared Loucksa676b5d2022-04-15 15:18:44 -0600686 """Get builds with successful UploadTestArtifacts stages in a given span.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700687
Xinan Lin71eeeb02020-03-10 17:37:12 -0700688 Same as get_passed_builds, we run the query twice to ensure we
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700689 fetched all builds from BQ for new_build event.
Xinan Lin3330d672020-03-03 14:52:36 -0800690
Xixuan Wuf856ff12019-05-21 14:09:38 -0700691 Args:
Xinan Linea1efcb2019-12-30 23:46:42 -0800692 earliest_end_time: a datetime.datetime object in UTC.
693 latest_end_time: a datetime.datetime object in UTC.
Xinan Lin71eeeb02020-03-10 17:37:12 -0700694 event_type: a string of event type.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700695
696 Returns:
697 A list of build_lib.BuildInfo objects.
698 """
Xinan Lin761b0c52020-03-25 17:31:57 -0700699 base_query_template = """
Sean McAllister53dd3d82021-05-18 15:15:14 -0600700 WITH builds AS
701 (SELECT
702 COALESCE(
703 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
704 JSON_EXTRACT_SCALAR(output.properties, '$.build_targets[0].name')
705 ) AS board,
706 COALESCE(
707 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
708 REPLACE(
709 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(0)], 'R', '')
710 ) AS milestone,
711 COALESCE(
712 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version'),
713 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(1)]
714 ) AS platform,
715 COALESCE(
716 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config'),
717 builder.builder
718 ) AS build_config,
719
720 step.name AS stage_name,
721
722 -- Time info
723 build.end_time as build_end_time,
724 CURRENT_TIMESTAMP() as inserted_time,
725 FROM `cr-buildbucket.chromeos.builds` build,
726 UNNEST(build.steps) AS step
727 WHERE
728 build.status != 'SUCCESS'
Jared Loucksa676b5d2022-04-15 15:18:44 -0600729 AND step.name = 'UploadTestArtifacts'
Sean McAllister53dd3d82021-05-18 15:15:14 -0600730 AND step.status = 'SUCCESS'
731 AND JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True')
732
733 SELECT
734 *,
735 '{0}' AS event_type
736 FROM
737 builds
738 WHERE board IS NOT NULL
Xixuan Wuf856ff12019-05-21 14:09:38 -0700739 """
Sean McAllister53dd3d82021-05-18 15:15:14 -0600740
Xinan Lin761b0c52020-03-25 17:31:57 -0700741 base_query_str = base_query_template.format(event_type)
Xinan Lin71eeeb02020-03-10 17:37:12 -0700742 earliest_end_time_str = earliest_end_time.strftime(
743 time_converter.TIME_FORMAT)
744 latest_end_time_str = latest_end_time.strftime(time_converter.TIME_FORMAT)
745 project_id = constants.AppID.STAGING_APP
746 if constants.environment() == constants.RunningEnv.ENV_PROD:
747 project_id = constants.application_id()
Xinan Lin3330d672020-03-03 14:52:36 -0800748
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700749 if event_type == 'new_build':
750 insert_relaxed_builds = """
751 INSERT
752 `google.com:{0}.builds.relaxed_builds`(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700753 stage_name,
754 board,
755 milestone,
756 platform,
757 build_config,
758 build_end_time,
759 inserted_time,
760 event_type
761 ) {1}
Sean McAllister909997a2021-05-19 13:28:25 -0600762 AND build_end_time > '{2}'
763 AND build_end_time < '{3}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700764 """
765 logging.info(
766 'Insert the visible relaxed builds '
767 'between %s and %s to BQ.', earliest_end_time_str,
768 latest_end_time_str)
Sean McAllister909997a2021-05-19 13:28:25 -0600769
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700770 self.query(
771 insert_relaxed_builds.format(project_id, base_query_str,
772 earliest_end_time_str,
773 latest_end_time_str))
Xixuan Wuf856ff12019-05-21 14:09:38 -0700774
Sean McAllister53dd3d82021-05-18 15:15:14 -0600775 query_template = _relaxed_build_query_template(event_type)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700776 query_str = query_template.format(base_query_str, earliest_end_time_str,
777 latest_end_time_str, project_id)
Xinan Lin3330d672020-03-03 14:52:36 -0800778 if global_config.GAE_TESTING:
Xinan Lin71eeeb02020-03-10 17:37:12 -0700779 query_str += 'LIMIT 10'
780 logging.info('Getting relaxed passed builds finished between %s and %s',
781 earliest_end_time_str, latest_end_time_str)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700782 res = self.query(query_str)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700783 res = _parse_bq_job_query(res)
784 if res is None:
785 return []
786
787 build_infos = []
Sean McAllister53dd3d82021-05-18 15:15:14 -0600788 for stage_name, board, milestone, platform, build_config in res:
Xixuan Wuf856ff12019-05-21 14:09:38 -0700789 build_infos.append(
Jared Loucksa676b5d2022-04-15 15:18:44 -0600790 build_lib.BuildInfo(board, None, milestone, platform, build_config))
Xixuan Wuf856ff12019-05-21 14:09:38 -0700791
792 return build_infos
793
794
Xixuan Wu55d38c52019-05-21 14:26:23 -0700795def _parse_bq_job_query(json_input):
796 """Parse response from API bigquery.jobs.query.
797
798 Args:
799 json_input: a dict, representing jsons returned by query API.
800
801 Returns:
802 A 2D string matrix: [rows[columns]], or None if no result.
803 E.g. Input:
804 "rows": [
805 {
806 "f": [ # field
807 {
808 "v": 'foo1',
809 },
810 {
811 "v": 'foo2',
812 }
813 ]
814 }
815 {
816 "f": [ # field
817 {
818 "v": 'bar1',
819 },
820 {
821 "v": 'bar2',
822 }
823 ]
824 }
825 ]
826 => Output: [['foo1', 'foo2'], ['bar1', 'bar2']]
827 """
828 if 'rows' not in json_input:
829 return None
830
831 res = []
832 for r in json_input['rows']:
833 rc = []
834 for c in r['f']:
835 rc.append(c['v'])
836
837 res.append(rc)
838
839 return res
Xixuan Wuf856ff12019-05-21 14:09:38 -0700840
Sean McAllister53dd3d82021-05-18 15:15:14 -0600841def _passed_build_query_template(event_type):
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700842 """Wrapper to create the query template for passed builds."""
843 if event_type == 'new_build':
844 return """
845 WITH passed_builds AS
846 (
847 {0}
848 AND (
Sean McAllister6525f662021-05-21 09:33:07 -0600849 (build_end_time > TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700850 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'), INTERVAL 1 DAY)
Sean McAllister6525f662021-05-21 09:33:07 -0600851 AND build_end_time < TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700852 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'), INTERVAL 1 DAY))
Sean McAllister6525f662021-05-21 09:33:07 -0600853 OR (build_end_time > '{1}' AND build_end_time < '{2}')
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700854 )
855 )
856 SELECT
857 b.board,
858 b.milestone,
859 b.platform,
860 b.build_config,
861 FROM
862 passed_builds AS b
863 LEFT JOIN
864 `google.com:{3}.builds.passed_builds` AS r
865 ON (
866 r.board = b.board
867 AND r.milestone = b.milestone
868 AND r.build_config = b.build_config
869 AND r.platform = b.platform
870 AND r.event_type = b.event_type
871 AND r.build_end_time > TIMESTAMP_SUB(
872 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'),
873 INTERVAL 1 DAY)
874 AND r.build_end_time < TIMESTAMP_SUB(
875 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'),
876 INTERVAL 1 DAY)
877 )
878 WHERE
879 r.inserted_time is null
880 """
881 return """
882 WITH passed_builds AS
883 (
884 {0}
Sean McAllister6525f662021-05-21 09:33:07 -0600885 AND build_end_time > '{1}'
886 AND build_end_time < '{2}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700887 )
888 SELECT
889 b.board,
890 b.milestone,
891 b.platform,
892 b.build_config,
893 FROM
894 passed_builds AS b
895 """
896
897
Sean McAllister53dd3d82021-05-18 15:15:14 -0600898def _relaxed_build_query_template(event_type):
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700899 """Wrapper to create the query template for relaxed builds."""
900 if event_type == 'new_build':
901 return """
902 WITH relaxed_builds AS
903 (
904 {0}
905 AND (
Sean McAllister6525f662021-05-21 09:33:07 -0600906 (build_end_time > TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700907 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'), INTERVAL 1 DAY)
Sean McAllister6525f662021-05-21 09:33:07 -0600908 AND build_end_time < TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700909 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'), INTERVAL 1 DAY))
Sean McAllister6525f662021-05-21 09:33:07 -0600910 OR (build_end_time > '{1}' AND build_end_time < '{2}')
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700911 )
912 )
913 SELECT
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700914 b.stage_name,
915 b.board,
916 b.milestone,
917 b.platform,
918 b.build_config,
919 FROM
920 relaxed_builds AS b
921 LEFT JOIN
922 `google.com:{3}.builds.relaxed_builds` AS r
923 ON (
924 r.board = b.board
925 AND r.milestone = b.milestone
926 AND r.build_config = b.build_config
927 AND r.platform = b.platform
928 AND r.event_type = b.event_type
929 AND r.build_end_time > TIMESTAMP_SUB(
930 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'),
931 INTERVAL 1 DAY)
932 AND r.build_end_time < TIMESTAMP_SUB(
933 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'),
934 INTERVAL 1 DAY)
935 )
936 WHERE
937 r.inserted_time is null
938 """
939 return """
940 WITH relaxed_builds AS
941 (
942 {0}
Sean McAllister6525f662021-05-21 09:33:07 -0600943 AND build_end_time > '{1}'
944 AND build_end_time < '{2}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700945 )
946 SELECT
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700947 b.stage_name,
948 b.board,
949 b.milestone,
950 b.platform,
951 b.build_config,
952 FROM
953 relaxed_builds AS b
954 """