blob: 836b438fa0d27fb6f66edcafc558944ad0a8d35f [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
Xixuan Wu5d6063e2017-09-05 16:15:07 -070058 def create_service(self, discovery_url=None):
59 """Create the service for a google API."""
60 self._init_credentials()
61 # Explicitly specify timeout for http to avoid DeadlineExceededError.
62 # It's used for services like AndroidBuild API, which raise such error
63 # when being triggered too many calls in a short time frame.
64 # http://stackoverflow.com/questions/14698119/httpexception-deadline-exceeded-while-waiting-for-http-response-from-url-dead
65 http_auth = self._credentials.authorize(httplib2.Http(timeout=30))
66 if discovery_url is None:
67 self._service = apiclient.discovery.build(
68 self.service_name, self.service_version,
69 http=http_auth)
70 else:
71 self._service = apiclient.discovery.build(
72 self.service_name, self.service_version, http=http_auth,
73 discoveryServiceUrl=discovery_url)
xixuan878b1eb2017-03-20 15:58:17 -070074
Xixuan Wu5d6063e2017-09-05 16:15:07 -070075 def _init_credentials(self):
76 """Initialize the credentials for a google API."""
77 if (self.running_env == constants.RunningEnv.ENV_STANDALONE or
78 self.running_env == constants.RunningEnv.ENV_DEVELOPMENT_SERVER):
79 # Running locally
80 service_credentials = service_account.ServiceAccountCredentials
81 self._credentials = service_credentials.from_json_keyfile_name(
Xixuan Wu26d06e02017-09-20 14:50:28 -070082 file_getter.STAGING_CLIENT_SECRETS_FILE, self.scopes)
Xixuan Wu5d6063e2017-09-05 16:15:07 -070083 else:
84 # Running in app-engine production
85 self._credentials = appengine.AppAssertionCredentials(self.scopes)
xixuan878b1eb2017-03-20 15:58:17 -070086
87
88class AndroidBuildRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070089 """REST client for android build API."""
xixuan878b1eb2017-03-20 15:58:17 -070090
Xixuan Wu5d6063e2017-09-05 16:15:07 -070091 def __init__(self, rest_client):
92 """Initialize a REST client for connecting to Android Build API."""
93 self._rest_client = rest_client
94 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -070095
Xixuan Wu5d6063e2017-09-05 16:15:07 -070096 def get_latest_build_id(self, branch, target):
97 """Get the latest build id for a given branch and target.
xixuan878b1eb2017-03-20 15:58:17 -070098
Xixuan Wu5d6063e2017-09-05 16:15:07 -070099 Args:
100 branch: an android build's branch
101 target: an android build's target
xixuan878b1eb2017-03-20 15:58:17 -0700102
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700103 Returns:
104 A string representing latest build id.
105 """
106 request = self._rest_client.service.build().list(
107 buildType='submitted',
108 branch=branch,
109 target=target,
110 successful=True,
111 maxResults=1)
112 builds = request.execute(num_retries=10)
113 if not builds or not builds['builds']:
114 return None
xixuan878b1eb2017-03-20 15:58:17 -0700115
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700116 return builds['builds'][0]['buildId']
xixuan878b1eb2017-03-20 15:58:17 -0700117
118
119class StorageRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700120 """REST client for google storage API."""
xixuan878b1eb2017-03-20 15:58:17 -0700121
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700122 def __init__(self, rest_client):
123 """Initialize a REST client for connecting to Google storage API."""
124 self._rest_client = rest_client
125 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -0700126
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700127 def read_object(self, bucket, object_path):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700128 """Read the contents of input_object in input_bucket.
xixuan878b1eb2017-03-20 15:58:17 -0700129
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700130 Args:
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700131 bucket: A string to indicate the bucket for fetching the object.
132 e.g. constants.StorageBucket.PROD_SUITE_SCHEDULER
133 object_path: A string to indicate the path of the object to read the
134 contents.
xixuan878b1eb2017-03-20 15:58:17 -0700135
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700136 Returns:
137 the stripped string contents of the input object.
xixuan878b1eb2017-03-20 15:58:17 -0700138
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700139 Raises:
140 apiclient.errors.HttpError
141 """
142 req = self._rest_client.service.objects().get_media(
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700143 bucket=bucket,
144 object=object_path)
145 return req.execute()
146
147 def upload_object(self, bucket, src_object_path, dest_object_path):
148 """Upload object_path to input_bucket.
149
150 Args:
151 bucket: A string to indicate the bucket for the object to be uploaded to.
152 src_object_path: A string the full path of the object to upload.
153 dest_object_path: A string path inside bucket to upload to.
154
155 Returns:
156 A dict of uploaded object info.
157
158 Raises:
159 apiclient.errors.HttpError
160 """
161 req = self._rest_client.service.objects().insert(
162 bucket=bucket,
163 name=dest_object_path,
164 media_body=src_object_path,
165 media_mime_type='text/plain',
166 )
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700167 return req.execute()
xixuan878b1eb2017-03-20 15:58:17 -0700168
169
170class CalendarRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700171 """Class of REST client for google calendar API."""
xixuan878b1eb2017-03-20 15:58:17 -0700172
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700173 def __init__(self, rest_client):
174 """Initialize a REST client for connecting to Google calendar API."""
175 self._rest_client = rest_client
176 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -0700177
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700178 def add_event(self, calendar_id, input_event):
179 """Add events of a given calendar.
xixuan878b1eb2017-03-20 15:58:17 -0700180
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700181 Args:
182 calendar_id: the ID of the given calendar.
183 input_event: the event to be added.
184 """
185 self._rest_client.service.events().insert(
186 calendarId=calendar_id,
187 body=input_event).execute()
xixuan878b1eb2017-03-20 15:58:17 -0700188
189
Xixuan Wu6f117e92017-10-27 10:51:58 -0700190class StackdriverRestClient(object):
191 """REST client for google storage API."""
192
193 def __init__(self, rest_client):
194 """Initialize a REST client for connecting to Google storage API."""
195 self._rest_client = rest_client
196 self._rest_client.create_service()
197
198 def read_logs(self, request):
Xinan Lin318cf752019-07-19 14:50:23 -0700199 # project_id, page_size, order_by, query_filter=''):
Xixuan Wu6f117e92017-10-27 10:51:58 -0700200 """Read the logs of the project_id based on all filters.
201
202 Args:
203 request: a request dict generated by
204 stackdriver_lib.form_logging_client_request.
205
206 Returns:
207 A json object, can be parsed by
208 stackdriver_lib.parse_logging_client_response.
209
210 Raises:
211 apiclient.errors.HttpError
212 """
213 req = self._rest_client.service.entries().list(
214 fields='entries/protoPayload', body=request)
215 return req.execute()
216
217
xixuan878b1eb2017-03-20 15:58:17 -0700218class SwarmingRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700219 """REST client for swarming proxy API."""
xixuan878b1eb2017-03-20 15:58:17 -0700220
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700221 DISCOVERY_URL_PATTERN = '%s/discovery/v1/apis/%s/%s/rest'
xixuan878b1eb2017-03-20 15:58:17 -0700222
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700223 def __init__(self, rest_client, service_url):
224 self._rest_client = rest_client
225 discovery_url = self.DISCOVERY_URL_PATTERN % (
226 service_url, rest_client.service_name, rest_client.service_version)
227 self._rest_client.create_service(discovery_url=discovery_url)
xixuan878b1eb2017-03-20 15:58:17 -0700228
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700229 def create_task(self, request):
230 """Create new task.
xixuan878b1eb2017-03-20 15:58:17 -0700231
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700232 Args:
233 request: a json-compatible dict expected by swarming server.
234 See _to_raw_request's output in swarming_lib.py for details.
xixuan878b1eb2017-03-20 15:58:17 -0700235
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700236 Returns:
237 A json dict returned by API task.new.
238 """
239 return self._rest_client.service.tasks().new(
240 fields='request,task_id', body=request).execute()
xixuan878b1eb2017-03-20 15:58:17 -0700241
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700242 def get_task_result(self, task_id):
243 """Get task results by a given task_id.
xixuan878b1eb2017-03-20 15:58:17 -0700244
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700245 Args:
246 task_id: A string, represents task id.
247
248 Returns:
249 A json dict returned by API task.result.
250 """
251 return self._rest_client.service.task().result(
252 task_id=task_id).execute()
Xixuan Wu7d142a92019-04-26 12:03:02 -0700253
254
255class BigqueryRestClient(object):
256 """Class of REST client for Bigquery API."""
257
Xixuan Wu55d38c52019-05-21 14:26:23 -0700258 PROJECT_TO_RUN_BIGQUERY_JOB = 'google.com:suite-scheduler'
259
Xinan Linc9f01152020-02-05 22:05:13 -0800260 def __init__(self, rest_client, project=None, dataset=None, table=None):
Xixuan Wu7d142a92019-04-26 12:03:02 -0700261 """Initialize a REST client for connecting to Bigquery API."""
262 self._rest_client = rest_client
263 self._rest_client.create_service()
Xinan Linc9f01152020-02-05 22:05:13 -0800264 self.project = project
265 self.dataset = dataset
266 self.table = table
Xixuan Wu7d142a92019-04-26 12:03:02 -0700267
Xixuan Wu55d38c52019-05-21 14:26:23 -0700268 def query(self, query_str):
269 """Query bigquery.
Xixuan Wu7d142a92019-04-26 12:03:02 -0700270
271 Args:
Xixuan Wu55d38c52019-05-21 14:26:23 -0700272 query_str: A string used to query Bigquery.
Xixuan Wu7d142a92019-04-26 12:03:02 -0700273
274 Returns:
275 A json dict returned by API bigquery.jobs.query, e.g.
276 # {...,
277 # "rows": [
278 # {
279 # "f": [ # field
280 # {
281 # "v": # value
Xixuan Wu55d38c52019-05-21 14:26:23 -0700282 # },
283 # {
284 # "v": # value
285 # },
286 # ...
Xixuan Wu7d142a92019-04-26 12:03:02 -0700287 # ]
288 # }
Xixuan Wuf856ff12019-05-21 14:09:38 -0700289 # ...
Xixuan Wu7d142a92019-04-26 12:03:02 -0700290 # ]
291 # }
292 """
293 query_data = {
Xixuan Wu55d38c52019-05-21 14:26:23 -0700294 'query': query_str,
295 'useLegacySql': False,
George Engelbrecht6c32e862022-02-11 23:35:20 -0700296 'timeoutMs': 200000,
Xixuan Wu7d142a92019-04-26 12:03:02 -0700297 }
Sean McAllister7d021782021-07-15 08:59:57 -0600298
299 for cnt in range(RETRY_LIMIT+1):
300 try:
301 return self._rest_client.service.jobs().query(
302 projectId=self.PROJECT_TO_RUN_BIGQUERY_JOB,
303 fields='rows',
304 body=query_data).execute()
305 except apiclient.errors.HttpError as ex:
306 status = ex.resp.status
307 if status in [500, 502, 503, 504]:
308 if cnt < RETRY_LIMIT:
309 logging.warning("Got response status %d, retrying" % status)
310 time.sleep(5)
311 else:
312 logging.error(
313 "Retry limit of %d hit communicating with BigQuery" % RETRY_LIMIT)
314 raise
315
Xixuan Wu55d38c52019-05-21 14:26:23 -0700316
Xinan Linc9f01152020-02-05 22:05:13 -0800317 def insert(self, rows):
318 """Insert rows to specified Bigquery table.
319
320 Args:
321 rows: list of json objects.
322
323 Raise:
324 RestClientError: if project/dataset/table is not defined.
325 """
326 if not any([self.project, self.dataset, self.table]):
327 raise RestClientError('Project, dataset, table should be all set.'
328 'Got project:%s, dataset:%s, table:%s' %
329 (self.project, self.dataset, self.table))
330 body = {
331 'kind': 'bigquery#tableDataInsertAllRequest',
332 'rows': rows,
333 }
334 request = self._rest_client.service.tabledata().insertAll(
335 projectId=self.project,
336 datasetId=self.dataset,
337 tableId=self.table,
338 body=body)
339 response = request.execute(num_retries=3)
340 if response.get('insertErrors'):
341 logging.error('InsertRequest reported errors: %r',
342 response.get('insertErrors'))
343 return False
344
345 return True
Xixuan Wu55d38c52019-05-21 14:26:23 -0700346
Xinan Lin80a9d932019-10-17 09:24:43 -0700347class CrOSTestPlatformBigqueryClient(BigqueryRestClient):
348 """REST client for cros_test_platform builder Bigquery API."""
Xixuan Wu55d38c52019-05-21 14:26:23 -0700349
Xinan Lin80a9d932019-10-17 09:24:43 -0700350 def get_past_job_nums(self, hours):
351 """Query the count of the jobs kicked off to cros_test_platform.
Xixuan Wu55d38c52019-05-21 14:26:23 -0700352
353 Args:
354 hours: An integer.
355
356 Returns:
357 An integer.
358 """
359 query_str = """
360 SELECT
361 COUNT(*)
362 FROM
Xinan Lin80a9d932019-10-17 09:24:43 -0700363 `cr-buildbucket.chromeos.builds`
Xixuan Wu55d38c52019-05-21 14:26:23 -0700364 WHERE
Xinan Lin80a9d932019-10-17 09:24:43 -0700365 created_by = 'user:suite-scheduler.google.com@appspot.gserviceaccount.com'
366 and create_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL %d HOUR);
Xixuan Wu55d38c52019-05-21 14:26:23 -0700367 """
368 res = self.query(query_str % hours)
369 try:
370 return int(_parse_bq_job_query(res)[0][0])
371 except (ValueError, KeyError) as e:
372 logging.debug('The returned json: \n%r', res)
373 logging.exception(str(e))
374 raise
375
376
Xixuan Wuf856ff12019-05-21 14:09:38 -0700377class BuildBucketBigqueryClient(BigqueryRestClient):
378 """Rest client for buildbucket Bigquery API."""
379
Xinan Lin028f9582019-12-11 10:55:33 -0800380 def get_latest_passed_firmware_builds(self):
381 """Get artifact link of the latest passed firmware builds for board.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700382
Xinan Lin028f9582019-12-11 10:55:33 -0800383 The query returns the latest firmware build for the combination of
384 board and build spec, which is cros or firmware. No restriction set
385 in the query, so it should return all available builds.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700386
387 Returns:
Xinan Lin028f9582019-12-11 10:55:33 -0800388 A list of (spec, board, firmware_artifact_link).
Xixuan Wuf856ff12019-05-21 14:09:38 -0700389 """
390 query_str = """
391 SELECT
Xinan Lin028f9582019-12-11 10:55:33 -0800392 spec,
393 board,
394 /*
395 * Firmware builds may contain artifacts for multiple boards in a
396 * single build - each in a separate directory.
397 */
398 IF(spec = 'firmware', CONCAT(artifact, '/', board), artifact) as artifact
Xixuan Wuf856ff12019-05-21 14:09:38 -0700399 FROM
Xinan Lin028f9582019-12-11 10:55:33 -0800400 (
401 SELECT
402 spec,
403 board,
404 artifact,
405 RANK() OVER (PARTITION BY spec, board ORDER BY end_time DESC) AS rank
406 FROM
407 (
408 SELECT
409 /*
410 * build_config is a string contains the board and build type.
411 * For Cros build, it has the form of "BoardName-release", while
412 * the firmware config shows like "firmware-BoardName-[1]-firmwarebranch".
413 * [1] is the firmware ver.
414 */
415 IF(prefix = 'firmware', 'firmware', 'cros') AS spec,
Jeremy Bettisd299c732022-02-23 12:41:03 -0700416 IF(prefix = 'firmware', COALESCE(
417 SPLIT(firmware_tarball, '/') [OFFSET(0)],
418 SPLIT(build_config, '-') [OFFSET(1)]
419 ), cros_prefix) AS board,
Xinan Lin028f9582019-12-11 10:55:33 -0800420 artifact,
421 end_time
422 FROM
423 (
424 SELECT
425 SPLIT(build_config, '-') [OFFSET(0)] AS prefix,
Jeremy Bettisd299c732022-02-23 12:41:03 -0700426 build_config,
Xinan Lin028f9582019-12-11 10:55:33 -0800427 REGEXP_EXTRACT(
428 build_config, r"(^[a-zA-Z0-9_.+-]+)-release"
429 ) as cros_prefix,
430 end_time,
Jeremy Bettisd299c732022-02-23 12:41:03 -0700431 artifact,
432 JSON_VALUE(fbb, '$') as firmware_tarball
Xinan Lin028f9582019-12-11 10:55:33 -0800433 FROM
434 (
435 SELECT
Sean McAllister53dd3d82021-05-18 15:15:14 -0600436 COALESCE(
437 JSON_EXTRACT_SCALAR(
438 output.properties, '$.artifact_link'
439 ),
440 FORMAT('gs://%s/%s',
441 JSON_EXTRACT_SCALAR(output.properties, '$.artifacts.gs_bucket'),
442 JSON_EXTRACT_SCALAR(output.properties, '$.artifacts.gs_path'))
Sean McAllister3fb4cec2021-04-20 22:38:38 +0000443 ) as artifact,
Sean McAllister53dd3d82021-05-18 15:15:14 -0600444 COALESCE(
445 JSON_EXTRACT_SCALAR(
446 output.properties, '$.cbb_config'
447 ),
448 builder.builder
Xinan Lin028f9582019-12-11 10:55:33 -0800449 ) as build_config,
Jeremy Bettisd299c732022-02-23 12:41:03 -0700450 end_time,
451 JSON_EXTRACT_ARRAY(output.properties, '$.artifacts.files_by_artifact.FIRMWARE_TARBALL') as firmware_by_board
Xinan Lin028f9582019-12-11 10:55:33 -0800452 FROM `cr-buildbucket.chromeos.completed_builds_BETA`
453 WHERE
454 status = 'SUCCESS'
455 AND JSON_EXTRACT_SCALAR(
456 output.properties, '$.suite_scheduling'
457 ) = 'True'
Jeremy Bettisd299c732022-02-23 12:41:03 -0700458 ) LEFT JOIN UNNEST(firmware_by_board) as fbb
Xinan Lin028f9582019-12-11 10:55:33 -0800459 )
460 )
461 )
462 WHERE rank = 1
Xixuan Wuf856ff12019-05-21 14:09:38 -0700463 """
Xinan Lin028f9582019-12-11 10:55:33 -0800464 res = self.query(query_str)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700465 res = _parse_bq_job_query(res)
466 if res is None:
467 return None
Xinan Lin028f9582019-12-11 10:55:33 -0800468 logging.info('Fetched the latest artifact links: %s',
469 [row[2] for row in res])
470 return res
Xinan Lin318cf752019-07-19 14:50:23 -0700471
Jack Neus8f0edb42022-03-17 20:21:39 +0000472 # TODO(b/225382624): Remove this when Rubik is the only source of builds.
Xinan Lin71eeeb02020-03-10 17:37:12 -0700473 def get_passed_builds(self, earliest_end_time, latest_end_time, event_type):
Xinan Linea1efcb2019-12-30 23:46:42 -0800474 """Get passed builds inside a given time span.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700475
Xinan Lin3330d672020-03-03 14:52:36 -0800476 BigQuery does not guarantee the inserted time of rows. A new build
477 may not get inserted when suite scheduler runs the query. To avoid
Xinan Lin71eeeb02020-03-10 17:37:12 -0700478 it, we scan each time span twice:
Xinan Lin3330d672020-03-03 14:52:36 -0800479 - the first run catches the new build from earliest_end_time to
480 latest_end_time, and inserts the result to a temp BQ table.
481 - the second run checks the build from (earliest_end_time - 1Day)
482 to (latest_end_time - 1Day) plus (earliest_end_time to
483 latest_end_time). The query returns the build which does not
484 appear in the temp table. Thus, if a build was not fetched by the
Xinan Lin71eeeb02020-03-10 17:37:12 -0700485 first run, we still could schedule test on it at most 1 day later
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700486 for new_build events.
487 Weekly and nightly events do not need this arrangement because
488 they do not cover every single build.
489
Xinan Lin3330d672020-03-03 14:52:36 -0800490
Xixuan Wuf856ff12019-05-21 14:09:38 -0700491 Args:
Xinan Linea1efcb2019-12-30 23:46:42 -0800492 earliest_end_time: a datetime.datetime object in UTC.
493 latest_end_time: a datetime.datetime object in UTC.
Xinan Lin71eeeb02020-03-10 17:37:12 -0700494 event_type: a string of event type. It could be one of
495 [WEEKLY|NIGHTLY|new_build].
Xixuan Wuf856ff12019-05-21 14:09:38 -0700496
497 Returns:
498 A list of build_lib.BuildInfo objects.
499 """
Xinan Lin761b0c52020-03-25 17:31:57 -0700500 base_query_template = """
Sean McAllister53dd3d82021-05-18 15:15:14 -0600501 WITH builds AS
502 (SELECT
503 COALESCE(
504 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
505 JSON_EXTRACT_SCALAR(output.properties, '$.build_targets[0].name')
506 ) AS board,
507 COALESCE(
508 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
509 REPLACE(
510 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(0)], 'R', '')
511 ) AS milestone,
512 COALESCE(
513 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version'),
514 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(1)]
515 ) AS platform,
516 COALESCE(
517 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config'),
518 builder.builder
519 ) AS build_config,
520
521 -- Time info
522 end_time as build_end_time,
523 CURRENT_TIMESTAMP() as inserted_time,
524 FROM `cr-buildbucket.chromeos.builds`
525 WHERE
526 status = 'SUCCESS'
527 AND JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True')
528
529 SELECT
530 *,
531 '{0}' as event_type
532 FROM builds
533 WHERE
534 board IS NOT NULL
Xixuan Wuf856ff12019-05-21 14:09:38 -0700535 """
Jack Neus8f0edb42022-03-17 20:21:39 +0000536 return self._get_passed_builds(base_query_template, earliest_end_time, latest_end_time, event_type)
537
538 def get_passed_rubik_builds(self, earliest_end_time, latest_end_time, event_type):
539 """Get passed Rubik builds inside a given time span.
540
541 BigQuery does not guarantee the inserted time of rows. A new build
542 may not get inserted when suite scheduler runs the query. To avoid
543 it, we scan each time span twice:
544 - the first run catches the new build from earliest_end_time to
545 latest_end_time, and inserts the result to a temp BQ table.
546 - the second run checks the build from (earliest_end_time - 1Day)
547 to (latest_end_time - 1Day) plus (earliest_end_time to
548 latest_end_time). The query returns the build which does not
549 appear in the temp table. Thus, if a build was not fetched by the
550 first run, we still could schedule test on it at most 1 day later
551 for new_build events.
552 Weekly and nightly events do not need this arrangement because
553 they do not cover every single build.
554
555
556 Args:
557 earliest_end_time: a datetime.datetime object in UTC.
558 latest_end_time: a datetime.datetime object in UTC.
559 event_type: a string of event type. It could be one of
560 [WEEKLY|NIGHTLY|new_build].
561
562 Returns:
563 A list of build_lib.BuildInfo objects.
564 """
565 base_query_template = """
566 WITH builds AS (
567 SELECT
568 JSON_EXTRACT_SCALAR(input.properties, '$.build_target.name') AS board,
Jack Neus6c270dd2022-03-18 20:02:48 +0000569 JSON_EXTRACT_SCALAR(output.properties, '$.target_versions.milestoneVersion') AS milestone,
Jack Neus8f0edb42022-03-17 20:21:39 +0000570 JSON_EXTRACT_SCALAR(output.properties, '$.target_versions.platformVersion') AS platform,
571 builder.builder AS build_config,
572 -- Time info
573 end_time as build_end_time,
574 CURRENT_TIMESTAMP() as inserted_time,
575 FROM `cr-buildbucket.chromeos.builds`
576 WHERE
577 status = 'SUCCESS' AND
578 JSON_EXTRACT_SCALAR(input.properties, '$.recipe') = 'build_release'
579 )
580 SELECT
581 *,
582 '{0}' as event_type
583 FROM builds
584 WHERE
585 board IS NOT NULL
586 """
587 return self._get_passed_builds(base_query_template, earliest_end_time, latest_end_time, event_type)
588
589 def _get_passed_builds(self, base_query_template, earliest_end_time, latest_end_time, event_type):
590 """Get passed builds inside a given time span.
591
592 BigQuery does not guarantee the inserted time of rows. A new build
593 may not get inserted when suite scheduler runs the query. To avoid
594 it, we scan each time span twice:
595 - the first run catches the new build from earliest_end_time to
596 latest_end_time, and inserts the result to a temp BQ table.
597 - the second run checks the build from (earliest_end_time - 1Day)
598 to (latest_end_time - 1Day) plus (earliest_end_time to
599 latest_end_time). The query returns the build which does not
600 appear in the temp table. Thus, if a build was not fetched by the
601 first run, we still could schedule test on it at most 1 day later
602 for new_build events.
603 Weekly and nightly events do not need this arrangement because
604 they do not cover every single build.
605
606
607 Args:
608 base_query_template: base query to use to find release builds.
609 earliest_end_time: a datetime.datetime object in UTC.
610 latest_end_time: a datetime.datetime object in UTC.
611 event_type: a string of event type. It could be one of
612 [WEEKLY|NIGHTLY|new_build].
613
614 Returns:
615 A list of build_lib.BuildInfo objects.
616 """
Xinan Lin761b0c52020-03-25 17:31:57 -0700617 base_query_str = base_query_template.format(event_type)
Xinan Lin71eeeb02020-03-10 17:37:12 -0700618 earliest_end_time_str = earliest_end_time.strftime(
619 time_converter.TIME_FORMAT)
620 latest_end_time_str = latest_end_time.strftime(time_converter.TIME_FORMAT)
621 project_id = constants.AppID.STAGING_APP
622 if constants.environment() == constants.RunningEnv.ENV_PROD:
623 project_id = constants.application_id()
Xinan Lin3330d672020-03-03 14:52:36 -0800624
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700625 if event_type == 'new_build':
626 insert_passed_builds = """
627 INSERT
628 `google.com:{0}.builds.passed_builds`(
629 board,
630 milestone,
631 platform,
632 build_config,
633 build_end_time,
634 inserted_time,
635 event_type
636 ) {1}
Sean McAllister909997a2021-05-19 13:28:25 -0600637 AND build_end_time > '{2}'
638 AND build_end_time < '{3}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700639 """
Sean McAllister909997a2021-05-19 13:28:25 -0600640
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700641 # Insert the currently visible builds to BQ.
642 logging.info(
643 'Insert the visible passed builds '
644 'between %s and %s to BQ.', earliest_end_time_str,
645 latest_end_time_str)
Sean McAllister909997a2021-05-19 13:28:25 -0600646
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700647 self.query(
648 insert_passed_builds.format(project_id, base_query_str,
649 earliest_end_time_str,
650 latest_end_time_str))
Xixuan Wuf856ff12019-05-21 14:09:38 -0700651
Sean McAllister53dd3d82021-05-18 15:15:14 -0600652 query_template = _passed_build_query_template(event_type)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700653 query_str = query_template.format(base_query_str, earliest_end_time_str,
654 latest_end_time_str, project_id)
Xinan Lin3330d672020-03-03 14:52:36 -0800655 if global_config.GAE_TESTING:
Xinan Lin71eeeb02020-03-10 17:37:12 -0700656 query_str += 'LIMIT 10'
657 logging.info('Getting passed builds finished between %s and %s',
658 earliest_end_time_str, latest_end_time_str)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700659 res = self.query(query_str)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700660 res = _parse_bq_job_query(res)
661 if res is None:
662 return []
663
664 build_infos = []
665 for board, milestone, platform, build_config in res:
Xixuan Wuf856ff12019-05-21 14:09:38 -0700666 build_infos.append(
667 build_lib.BuildInfo(board, None, milestone, platform, build_config))
668
669 return build_infos
670
Xinan Lin71eeeb02020-03-10 17:37:12 -0700671 def get_relaxed_passed_builds(self, earliest_end_time, latest_end_time,
672 event_type):
Sean McAllister53dd3d82021-05-18 15:15:14 -0600673 """Get builds with successful SkylabHWTest stages between a given span.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700674
Xinan Lin71eeeb02020-03-10 17:37:12 -0700675 Same as get_passed_builds, we run the query twice to ensure we
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700676 fetched all builds from BQ for new_build event.
Xinan Lin3330d672020-03-03 14:52:36 -0800677
Xixuan Wuf856ff12019-05-21 14:09:38 -0700678 Args:
Xinan Linea1efcb2019-12-30 23:46:42 -0800679 earliest_end_time: a datetime.datetime object in UTC.
680 latest_end_time: a datetime.datetime object in UTC.
Xinan Lin71eeeb02020-03-10 17:37:12 -0700681 event_type: a string of event type.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700682
683 Returns:
684 A list of build_lib.BuildInfo objects.
685 """
Xinan Lin761b0c52020-03-25 17:31:57 -0700686 base_query_template = """
Sean McAllister53dd3d82021-05-18 15:15:14 -0600687 WITH builds AS
688 (SELECT
689 COALESCE(
690 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
691 JSON_EXTRACT_SCALAR(output.properties, '$.build_targets[0].name')
692 ) AS board,
693 COALESCE(
694 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
695 REPLACE(
696 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(0)], 'R', '')
697 ) AS milestone,
698 COALESCE(
699 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version'),
700 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(1)]
701 ) AS platform,
702 COALESCE(
703 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config'),
704 builder.builder
705 ) AS build_config,
706
707 step.name AS stage_name,
708
709 -- Time info
710 build.end_time as build_end_time,
711 CURRENT_TIMESTAMP() as inserted_time,
712 FROM `cr-buildbucket.chromeos.builds` build,
713 UNNEST(build.steps) AS step
714 WHERE
715 build.status != 'SUCCESS'
716 AND step.name like 'SkylabHWTest%%'
717 AND step.status = 'SUCCESS'
718 AND JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True')
719
720 SELECT
721 *,
722 '{0}' AS event_type
723 FROM
724 builds
725 WHERE board IS NOT NULL
Xixuan Wuf856ff12019-05-21 14:09:38 -0700726 """
Sean McAllister53dd3d82021-05-18 15:15:14 -0600727
Xinan Lin761b0c52020-03-25 17:31:57 -0700728 base_query_str = base_query_template.format(event_type)
Xinan Lin71eeeb02020-03-10 17:37:12 -0700729 earliest_end_time_str = earliest_end_time.strftime(
730 time_converter.TIME_FORMAT)
731 latest_end_time_str = latest_end_time.strftime(time_converter.TIME_FORMAT)
732 project_id = constants.AppID.STAGING_APP
733 if constants.environment() == constants.RunningEnv.ENV_PROD:
734 project_id = constants.application_id()
Xinan Lin3330d672020-03-03 14:52:36 -0800735
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700736 if event_type == 'new_build':
737 insert_relaxed_builds = """
738 INSERT
739 `google.com:{0}.builds.relaxed_builds`(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700740 stage_name,
741 board,
742 milestone,
743 platform,
744 build_config,
745 build_end_time,
746 inserted_time,
747 event_type
748 ) {1}
Sean McAllister909997a2021-05-19 13:28:25 -0600749 AND build_end_time > '{2}'
750 AND build_end_time < '{3}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700751 """
752 logging.info(
753 'Insert the visible relaxed builds '
754 'between %s and %s to BQ.', earliest_end_time_str,
755 latest_end_time_str)
Sean McAllister909997a2021-05-19 13:28:25 -0600756
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700757 self.query(
758 insert_relaxed_builds.format(project_id, base_query_str,
759 earliest_end_time_str,
760 latest_end_time_str))
Xixuan Wuf856ff12019-05-21 14:09:38 -0700761
Sean McAllister53dd3d82021-05-18 15:15:14 -0600762 query_template = _relaxed_build_query_template(event_type)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700763 query_str = query_template.format(base_query_str, earliest_end_time_str,
764 latest_end_time_str, project_id)
Xinan Lin3330d672020-03-03 14:52:36 -0800765 if global_config.GAE_TESTING:
Xinan Lin71eeeb02020-03-10 17:37:12 -0700766 query_str += 'LIMIT 10'
767 logging.info('Getting relaxed passed builds finished between %s and %s',
768 earliest_end_time_str, latest_end_time_str)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700769 res = self.query(query_str)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700770 res = _parse_bq_job_query(res)
771 if res is None:
772 return []
773
774 build_infos = []
Sean McAllister53dd3d82021-05-18 15:15:14 -0600775 for stage_name, board, milestone, platform, build_config in res:
776 model = _try_parse_model(stage_name)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700777 build_infos.append(
778 build_lib.BuildInfo(board, model, milestone, platform, build_config))
779
780 return build_infos
781
782
Xixuan Wu55d38c52019-05-21 14:26:23 -0700783def _parse_bq_job_query(json_input):
784 """Parse response from API bigquery.jobs.query.
785
786 Args:
787 json_input: a dict, representing jsons returned by query API.
788
789 Returns:
790 A 2D string matrix: [rows[columns]], or None if no result.
791 E.g. Input:
792 "rows": [
793 {
794 "f": [ # field
795 {
796 "v": 'foo1',
797 },
798 {
799 "v": 'foo2',
800 }
801 ]
802 }
803 {
804 "f": [ # field
805 {
806 "v": 'bar1',
807 },
808 {
809 "v": 'bar2',
810 }
811 ]
812 }
813 ]
814 => Output: [['foo1', 'foo2'], ['bar1', 'bar2']]
815 """
816 if 'rows' not in json_input:
817 return None
818
819 res = []
820 for r in json_input['rows']:
821 rc = []
822 for c in r['f']:
823 rc.append(c['v'])
824
825 res.append(rc)
826
827 return res
Xixuan Wuf856ff12019-05-21 14:09:38 -0700828
829
Sean McAllister53dd3d82021-05-18 15:15:14 -0600830def _try_parse_model(build_stage_name):
831 """Try to parse model name from the SkylabHWTest stage name.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700832
Sean McAllister53dd3d82021-05-18 15:15:14 -0600833 An example build_stage_name is 'HWTest [bvt-isntaller] [whitetip]'. So we'll
834 regexp match on this to get the board name out of the second bracket.
835
Xixuan Wuf856ff12019-05-21 14:09:38 -0700836 Args:
837 build_stage_name: The stage name of a HWTest sanity stage, e.g.
Sean McAllister53dd3d82021-05-18 15:15:14 -0600838 "SkylabHWTest [bvt-installer] [whitetip]".
Xixuan Wuf856ff12019-05-21 14:09:38 -0700839
840 Returns:
Sean McAllister53dd3d82021-05-18 15:15:14 -0600841 A model name, e.g. "whitetip" or None if not found.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700842 """
Xixuan Wuf856ff12019-05-21 14:09:38 -0700843
Sean McAllister53dd3d82021-05-18 15:15:14 -0600844 match = re.search(
845 "SkylabHWTest \[[a-z0-9-]+\] \[([a-z0-9-]+)\]",
846 build_stage_name,
847 )
Xixuan Wuf856ff12019-05-21 14:09:38 -0700848
Sean McAllister53dd3d82021-05-18 15:15:14 -0600849 if match:
Sean McAllisterdb8c7232021-05-21 11:26:51 -0600850 return match.group(1)
Sean McAllister53dd3d82021-05-18 15:15:14 -0600851 return None
Xixuan Wuf856ff12019-05-21 14:09:38 -0700852
Sean McAllister53dd3d82021-05-18 15:15:14 -0600853def _passed_build_query_template(event_type):
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700854 """Wrapper to create the query template for passed builds."""
855 if event_type == 'new_build':
856 return """
857 WITH passed_builds AS
858 (
859 {0}
860 AND (
Sean McAllister6525f662021-05-21 09:33:07 -0600861 (build_end_time > TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700862 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'), INTERVAL 1 DAY)
Sean McAllister6525f662021-05-21 09:33:07 -0600863 AND build_end_time < TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700864 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'), INTERVAL 1 DAY))
Sean McAllister6525f662021-05-21 09:33:07 -0600865 OR (build_end_time > '{1}' AND build_end_time < '{2}')
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700866 )
867 )
868 SELECT
869 b.board,
870 b.milestone,
871 b.platform,
872 b.build_config,
873 FROM
874 passed_builds AS b
875 LEFT JOIN
876 `google.com:{3}.builds.passed_builds` AS r
877 ON (
878 r.board = b.board
879 AND r.milestone = b.milestone
880 AND r.build_config = b.build_config
881 AND r.platform = b.platform
882 AND r.event_type = b.event_type
883 AND r.build_end_time > TIMESTAMP_SUB(
884 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'),
885 INTERVAL 1 DAY)
886 AND r.build_end_time < TIMESTAMP_SUB(
887 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'),
888 INTERVAL 1 DAY)
889 )
890 WHERE
891 r.inserted_time is null
892 """
893 return """
894 WITH passed_builds AS
895 (
896 {0}
Sean McAllister6525f662021-05-21 09:33:07 -0600897 AND build_end_time > '{1}'
898 AND build_end_time < '{2}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700899 )
900 SELECT
901 b.board,
902 b.milestone,
903 b.platform,
904 b.build_config,
905 FROM
906 passed_builds AS b
907 """
908
909
Sean McAllister53dd3d82021-05-18 15:15:14 -0600910def _relaxed_build_query_template(event_type):
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700911 """Wrapper to create the query template for relaxed builds."""
912 if event_type == 'new_build':
913 return """
914 WITH relaxed_builds AS
915 (
916 {0}
917 AND (
Sean McAllister6525f662021-05-21 09:33:07 -0600918 (build_end_time > TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700919 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'), INTERVAL 1 DAY)
Sean McAllister6525f662021-05-21 09:33:07 -0600920 AND build_end_time < TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700921 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'), INTERVAL 1 DAY))
Sean McAllister6525f662021-05-21 09:33:07 -0600922 OR (build_end_time > '{1}' AND build_end_time < '{2}')
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700923 )
924 )
925 SELECT
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700926 b.stage_name,
927 b.board,
928 b.milestone,
929 b.platform,
930 b.build_config,
931 FROM
932 relaxed_builds AS b
933 LEFT JOIN
934 `google.com:{3}.builds.relaxed_builds` AS r
935 ON (
936 r.board = b.board
937 AND r.milestone = b.milestone
938 AND r.build_config = b.build_config
939 AND r.platform = b.platform
940 AND r.event_type = b.event_type
941 AND r.build_end_time > TIMESTAMP_SUB(
942 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'),
943 INTERVAL 1 DAY)
944 AND r.build_end_time < TIMESTAMP_SUB(
945 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'),
946 INTERVAL 1 DAY)
947 )
948 WHERE
949 r.inserted_time is null
950 """
951 return """
952 WITH relaxed_builds AS
953 (
954 {0}
Sean McAllister6525f662021-05-21 09:33:07 -0600955 AND build_end_time > '{1}'
956 AND build_end_time < '{2}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700957 )
958 SELECT
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700959 b.stage_name,
960 b.board,
961 b.milestone,
962 b.platform,
963 b.build_config,
964 FROM
965 relaxed_builds AS b
966 """