blob: c9f57bac765e39ee66de58ecd97de573c02e9a01 [file] [log] [blame]
xixuan878b1eb2017-03-20 15:58:17 -07001# Copyright 2017 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Module for interacting with google APIs."""
Xixuan Wu5d6063e2017-09-05 16:15:07 -07006# pylint: disable=g-bad-import-order
Xixuan Wud55ac6e2019-03-14 10:56:39 -07007# pylint: disable=g-bad-exception-name
Xixuan Wu5d6063e2017-09-05 16:15:07 -07008
Xixuan Wuf856ff12019-05-21 14:09:38 -07009import ast
Xixuan Wu5d6063e2017-09-05 16:15:07 -070010import httplib2
Xixuan Wu55d38c52019-05-21 14:26:23 -070011import logging
Xixuan Wuf856ff12019-05-21 14:09:38 -070012import re
Sean McAllister7d021782021-07-15 08:59:57 -060013import time
xixuan878b1eb2017-03-20 15:58:17 -070014
15import apiclient
Xixuan Wuf856ff12019-05-21 14:09:38 -070016import build_lib
xixuan878b1eb2017-03-20 15:58:17 -070017import constants
18import file_getter
Xixuan Wuf856ff12019-05-21 14:09:38 -070019import global_config
20import time_converter
xixuan878b1eb2017-03-20 15:58:17 -070021
22from oauth2client import service_account
23from oauth2client.contrib import appengine
24
Sean McAllister7d021782021-07-15 08:59:57 -060025RETRY_LIMIT = 3
26
xixuan878b1eb2017-03-20 15:58:17 -070027
28class RestClientError(Exception):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070029 """Raised when there is a general error."""
xixuan878b1eb2017-03-20 15:58:17 -070030
31
32class NoServiceRestClientError(RestClientError):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070033 """Raised when there is no ready service for a google API."""
xixuan878b1eb2017-03-20 15:58:17 -070034
35
36class BaseRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070037 """Base class of REST client for google APIs."""
xixuan878b1eb2017-03-20 15:58:17 -070038
Xixuan Wu5d6063e2017-09-05 16:15:07 -070039 def __init__(self, scopes, service_name, service_version):
40 """Initialize a REST client to connect to a google API.
xixuan878b1eb2017-03-20 15:58:17 -070041
Xixuan Wu5d6063e2017-09-05 16:15:07 -070042 Args:
43 scopes: the scopes of the to-be-connected API.
44 service_name: the service name of the to-be-connected API.
45 service_version: the service version of the to-be-connected API.
46 """
47 self.running_env = constants.environment()
48 self.scopes = scopes
49 self.service_name = service_name
50 self.service_version = service_version
xixuan878b1eb2017-03-20 15:58:17 -070051
Xixuan Wu5d6063e2017-09-05 16:15:07 -070052 @property
53 def service(self):
54 if not self._service:
55 raise NoServiceRestClientError('No service created for calling API')
xixuan878b1eb2017-03-20 15:58:17 -070056
Xixuan Wu5d6063e2017-09-05 16:15:07 -070057 return self._service
xixuan878b1eb2017-03-20 15:58:17 -070058
Xixuan Wu5d6063e2017-09-05 16:15:07 -070059 def create_service(self, discovery_url=None):
60 """Create the service for a google API."""
61 self._init_credentials()
62 # 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
66 http_auth = self._credentials.authorize(httplib2.Http(timeout=30))
67 if discovery_url is None:
68 self._service = apiclient.discovery.build(
69 self.service_name, self.service_version,
70 http=http_auth)
71 else:
72 self._service = apiclient.discovery.build(
73 self.service_name, self.service_version, http=http_auth,
74 discoveryServiceUrl=discovery_url)
xixuan878b1eb2017-03-20 15:58:17 -070075
Xixuan Wu5d6063e2017-09-05 16:15:07 -070076 def _init_credentials(self):
77 """Initialize the credentials for a google API."""
78 if (self.running_env == constants.RunningEnv.ENV_STANDALONE or
79 self.running_env == constants.RunningEnv.ENV_DEVELOPMENT_SERVER):
80 # Running locally
81 service_credentials = service_account.ServiceAccountCredentials
82 self._credentials = service_credentials.from_json_keyfile_name(
Xixuan Wu26d06e02017-09-20 14:50:28 -070083 file_getter.STAGING_CLIENT_SECRETS_FILE, self.scopes)
Xixuan Wu5d6063e2017-09-05 16:15:07 -070084 else:
85 # Running in app-engine production
86 self._credentials = appengine.AppAssertionCredentials(self.scopes)
xixuan878b1eb2017-03-20 15:58:17 -070087
88
89class AndroidBuildRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070090 """REST client for android build API."""
xixuan878b1eb2017-03-20 15:58:17 -070091
Xixuan Wu5d6063e2017-09-05 16:15:07 -070092 def __init__(self, rest_client):
93 """Initialize a REST client for connecting to Android Build API."""
94 self._rest_client = rest_client
95 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -070096
Xixuan Wu5d6063e2017-09-05 16:15:07 -070097 def get_latest_build_id(self, branch, target):
98 """Get the latest build id for a given branch and target.
xixuan878b1eb2017-03-20 15:58:17 -070099
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700100 Args:
101 branch: an android build's branch
102 target: an android build's target
xixuan878b1eb2017-03-20 15:58:17 -0700103
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700104 Returns:
105 A string representing latest build id.
106 """
107 request = self._rest_client.service.build().list(
108 buildType='submitted',
109 branch=branch,
110 target=target,
111 successful=True,
112 maxResults=1)
113 builds = request.execute(num_retries=10)
114 if not builds or not builds['builds']:
115 return None
xixuan878b1eb2017-03-20 15:58:17 -0700116
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700117 return builds['builds'][0]['buildId']
xixuan878b1eb2017-03-20 15:58:17 -0700118
119
120class StorageRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700121 """REST client for google storage API."""
xixuan878b1eb2017-03-20 15:58:17 -0700122
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700123 def __init__(self, rest_client):
124 """Initialize a REST client for connecting to Google storage API."""
125 self._rest_client = rest_client
126 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -0700127
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700128 def read_object(self, bucket, object_path):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700129 """Read the contents of input_object in input_bucket.
xixuan878b1eb2017-03-20 15:58:17 -0700130
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700131 Args:
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700132 bucket: A string to indicate the bucket for fetching the object.
133 e.g. constants.StorageBucket.PROD_SUITE_SCHEDULER
134 object_path: A string to indicate the path of the object to read the
135 contents.
xixuan878b1eb2017-03-20 15:58:17 -0700136
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700137 Returns:
138 the stripped string contents of the input object.
xixuan878b1eb2017-03-20 15:58:17 -0700139
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700140 Raises:
141 apiclient.errors.HttpError
142 """
143 req = self._rest_client.service.objects().get_media(
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700144 bucket=bucket,
145 object=object_path)
146 return req.execute()
147
148 def upload_object(self, bucket, src_object_path, dest_object_path):
149 """Upload object_path to input_bucket.
150
151 Args:
152 bucket: A string to indicate the bucket for the object to be uploaded to.
153 src_object_path: A string the full path of the object to upload.
154 dest_object_path: A string path inside bucket to upload to.
155
156 Returns:
157 A dict of uploaded object info.
158
159 Raises:
160 apiclient.errors.HttpError
161 """
162 req = self._rest_client.service.objects().insert(
163 bucket=bucket,
164 name=dest_object_path,
165 media_body=src_object_path,
166 media_mime_type='text/plain',
167 )
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700168 return req.execute()
xixuan878b1eb2017-03-20 15:58:17 -0700169
170
171class CalendarRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700172 """Class of REST client for google calendar API."""
xixuan878b1eb2017-03-20 15:58:17 -0700173
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700174 def __init__(self, rest_client):
175 """Initialize a REST client for connecting to Google calendar API."""
176 self._rest_client = rest_client
177 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -0700178
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700179 def add_event(self, calendar_id, input_event):
180 """Add events of a given calendar.
xixuan878b1eb2017-03-20 15:58:17 -0700181
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700182 Args:
183 calendar_id: the ID of the given calendar.
184 input_event: the event to be added.
185 """
186 self._rest_client.service.events().insert(
187 calendarId=calendar_id,
188 body=input_event).execute()
xixuan878b1eb2017-03-20 15:58:17 -0700189
190
Xixuan Wu6f117e92017-10-27 10:51:58 -0700191class StackdriverRestClient(object):
192 """REST client for google storage API."""
193
194 def __init__(self, rest_client):
195 """Initialize a REST client for connecting to Google storage API."""
196 self._rest_client = rest_client
197 self._rest_client.create_service()
198
199 def read_logs(self, request):
Xinan Lin318cf752019-07-19 14:50:23 -0700200 # project_id, page_size, order_by, query_filter=''):
Xixuan Wu6f117e92017-10-27 10:51:58 -0700201 """Read the logs of the project_id based on all filters.
202
203 Args:
204 request: a request dict generated by
205 stackdriver_lib.form_logging_client_request.
206
207 Returns:
208 A json object, can be parsed by
209 stackdriver_lib.parse_logging_client_response.
210
211 Raises:
212 apiclient.errors.HttpError
213 """
214 req = self._rest_client.service.entries().list(
215 fields='entries/protoPayload', body=request)
216 return req.execute()
217
218
xixuan878b1eb2017-03-20 15:58:17 -0700219class SwarmingRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700220 """REST client for swarming proxy API."""
xixuan878b1eb2017-03-20 15:58:17 -0700221
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700222 DISCOVERY_URL_PATTERN = '%s/discovery/v1/apis/%s/%s/rest'
xixuan878b1eb2017-03-20 15:58:17 -0700223
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700224 def __init__(self, rest_client, service_url):
225 self._rest_client = rest_client
226 discovery_url = self.DISCOVERY_URL_PATTERN % (
227 service_url, rest_client.service_name, rest_client.service_version)
228 self._rest_client.create_service(discovery_url=discovery_url)
xixuan878b1eb2017-03-20 15:58:17 -0700229
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700230 def create_task(self, request):
231 """Create new task.
xixuan878b1eb2017-03-20 15:58:17 -0700232
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700233 Args:
234 request: a json-compatible dict expected by swarming server.
235 See _to_raw_request's output in swarming_lib.py for details.
xixuan878b1eb2017-03-20 15:58:17 -0700236
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700237 Returns:
238 A json dict returned by API task.new.
239 """
240 return self._rest_client.service.tasks().new(
241 fields='request,task_id', body=request).execute()
xixuan878b1eb2017-03-20 15:58:17 -0700242
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700243 def get_task_result(self, task_id):
244 """Get task results by a given task_id.
xixuan878b1eb2017-03-20 15:58:17 -0700245
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700246 Args:
247 task_id: A string, represents task id.
248
249 Returns:
250 A json dict returned by API task.result.
251 """
252 return self._rest_client.service.task().result(
253 task_id=task_id).execute()
Xixuan Wu7d142a92019-04-26 12:03:02 -0700254
255
256class BigqueryRestClient(object):
257 """Class of REST client for Bigquery API."""
258
Xixuan Wu55d38c52019-05-21 14:26:23 -0700259 PROJECT_TO_RUN_BIGQUERY_JOB = 'google.com:suite-scheduler'
260
Xinan Linc9f01152020-02-05 22:05:13 -0800261 def __init__(self, rest_client, project=None, dataset=None, table=None):
Xixuan Wu7d142a92019-04-26 12:03:02 -0700262 """Initialize a REST client for connecting to Bigquery API."""
263 self._rest_client = rest_client
264 self._rest_client.create_service()
Xinan Linc9f01152020-02-05 22:05:13 -0800265 self.project = project
266 self.dataset = dataset
267 self.table = table
Xixuan Wu7d142a92019-04-26 12:03:02 -0700268
Xixuan Wu55d38c52019-05-21 14:26:23 -0700269 def query(self, query_str):
270 """Query bigquery.
Xixuan Wu7d142a92019-04-26 12:03:02 -0700271
272 Args:
Xixuan Wu55d38c52019-05-21 14:26:23 -0700273 query_str: A string used to query Bigquery.
Xixuan Wu7d142a92019-04-26 12:03:02 -0700274
275 Returns:
276 A json dict returned by API bigquery.jobs.query, e.g.
277 # {...,
278 # "rows": [
279 # {
280 # "f": [ # field
281 # {
282 # "v": # value
Xixuan Wu55d38c52019-05-21 14:26:23 -0700283 # },
284 # {
285 # "v": # value
286 # },
287 # ...
Xixuan Wu7d142a92019-04-26 12:03:02 -0700288 # ]
289 # }
Xixuan Wuf856ff12019-05-21 14:09:38 -0700290 # ...
Xixuan Wu7d142a92019-04-26 12:03:02 -0700291 # ]
292 # }
293 """
294 query_data = {
Xixuan Wu55d38c52019-05-21 14:26:23 -0700295 'query': query_str,
296 'useLegacySql': False,
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,
416 IF(prefix = 'firmware', firmware_post_prefix, cros_prefix) AS board,
417 artifact,
418 end_time
419 FROM
420 (
421 SELECT
422 SPLIT(build_config, '-') [OFFSET(0)] AS prefix,
423 SPLIT(build_config, '-') [OFFSET(1)] AS firmware_post_prefix,
424 REGEXP_EXTRACT(
425 build_config, r"(^[a-zA-Z0-9_.+-]+)-release"
426 ) as cros_prefix,
427 end_time,
428 artifact
429 FROM
430 (
431 SELECT
Sean McAllister53dd3d82021-05-18 15:15:14 -0600432 COALESCE(
433 JSON_EXTRACT_SCALAR(
434 output.properties, '$.artifact_link'
435 ),
436 FORMAT('gs://%s/%s',
437 JSON_EXTRACT_SCALAR(output.properties, '$.artifacts.gs_bucket'),
438 JSON_EXTRACT_SCALAR(output.properties, '$.artifacts.gs_path'))
Sean McAllister3fb4cec2021-04-20 22:38:38 +0000439 ) as artifact,
Sean McAllister53dd3d82021-05-18 15:15:14 -0600440 COALESCE(
441 JSON_EXTRACT_SCALAR(
442 output.properties, '$.cbb_config'
443 ),
444 builder.builder
Xinan Lin028f9582019-12-11 10:55:33 -0800445 ) as build_config,
446 end_time
447 FROM `cr-buildbucket.chromeos.completed_builds_BETA`
448 WHERE
449 status = 'SUCCESS'
450 AND JSON_EXTRACT_SCALAR(
451 output.properties, '$.suite_scheduling'
452 ) = 'True'
453 )
454 )
455 )
456 )
457 WHERE rank = 1
Xixuan Wuf856ff12019-05-21 14:09:38 -0700458 """
Xinan Lin028f9582019-12-11 10:55:33 -0800459 res = self.query(query_str)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700460 res = _parse_bq_job_query(res)
461 if res is None:
462 return None
Xinan Lin028f9582019-12-11 10:55:33 -0800463 logging.info('Fetched the latest artifact links: %s',
464 [row[2] for row in res])
465 return res
Xinan Lin318cf752019-07-19 14:50:23 -0700466
Xinan Lin71eeeb02020-03-10 17:37:12 -0700467 def get_passed_builds(self, earliest_end_time, latest_end_time, event_type):
Xinan Linea1efcb2019-12-30 23:46:42 -0800468 """Get passed builds inside a given time span.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700469
Xinan Lin3330d672020-03-03 14:52:36 -0800470 BigQuery does not guarantee the inserted time of rows. A new build
471 may not get inserted when suite scheduler runs the query. To avoid
Xinan Lin71eeeb02020-03-10 17:37:12 -0700472 it, we scan each time span twice:
Xinan Lin3330d672020-03-03 14:52:36 -0800473 - the first run catches the new build from earliest_end_time to
474 latest_end_time, and inserts the result to a temp BQ table.
475 - the second run checks the build from (earliest_end_time - 1Day)
476 to (latest_end_time - 1Day) plus (earliest_end_time to
477 latest_end_time). The query returns the build which does not
478 appear in the temp table. Thus, if a build was not fetched by the
Xinan Lin71eeeb02020-03-10 17:37:12 -0700479 first run, we still could schedule test on it at most 1 day later
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700480 for new_build events.
481 Weekly and nightly events do not need this arrangement because
482 they do not cover every single build.
483
Xinan Lin3330d672020-03-03 14:52:36 -0800484
Xixuan Wuf856ff12019-05-21 14:09:38 -0700485 Args:
Xinan Linea1efcb2019-12-30 23:46:42 -0800486 earliest_end_time: a datetime.datetime object in UTC.
487 latest_end_time: a datetime.datetime object in UTC.
Xinan Lin71eeeb02020-03-10 17:37:12 -0700488 event_type: a string of event type. It could be one of
489 [WEEKLY|NIGHTLY|new_build].
Xixuan Wuf856ff12019-05-21 14:09:38 -0700490
491 Returns:
492 A list of build_lib.BuildInfo objects.
493 """
Xinan Lin761b0c52020-03-25 17:31:57 -0700494 base_query_template = """
Sean McAllister53dd3d82021-05-18 15:15:14 -0600495 WITH builds AS
496 (SELECT
497 COALESCE(
498 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
499 JSON_EXTRACT_SCALAR(output.properties, '$.build_targets[0].name')
500 ) AS board,
501 COALESCE(
502 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
503 REPLACE(
504 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(0)], 'R', '')
505 ) AS milestone,
506 COALESCE(
507 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version'),
508 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(1)]
509 ) AS platform,
510 COALESCE(
511 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config'),
512 builder.builder
513 ) AS build_config,
514
515 -- Time info
516 end_time as build_end_time,
517 CURRENT_TIMESTAMP() as inserted_time,
518 FROM `cr-buildbucket.chromeos.builds`
519 WHERE
520 status = 'SUCCESS'
521 AND JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True')
522
523 SELECT
524 *,
525 '{0}' as event_type
526 FROM builds
527 WHERE
528 board IS NOT NULL
Xixuan Wuf856ff12019-05-21 14:09:38 -0700529 """
Xinan Lin761b0c52020-03-25 17:31:57 -0700530 base_query_str = base_query_template.format(event_type)
Xinan Lin71eeeb02020-03-10 17:37:12 -0700531 earliest_end_time_str = earliest_end_time.strftime(
532 time_converter.TIME_FORMAT)
533 latest_end_time_str = latest_end_time.strftime(time_converter.TIME_FORMAT)
534 project_id = constants.AppID.STAGING_APP
535 if constants.environment() == constants.RunningEnv.ENV_PROD:
536 project_id = constants.application_id()
Xinan Lin3330d672020-03-03 14:52:36 -0800537
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700538 if event_type == 'new_build':
539 insert_passed_builds = """
540 INSERT
541 `google.com:{0}.builds.passed_builds`(
542 board,
543 milestone,
544 platform,
545 build_config,
546 build_end_time,
547 inserted_time,
548 event_type
549 ) {1}
Sean McAllister909997a2021-05-19 13:28:25 -0600550 AND build_end_time > '{2}'
551 AND build_end_time < '{3}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700552 """
Sean McAllister909997a2021-05-19 13:28:25 -0600553
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700554 # Insert the currently visible builds to BQ.
555 logging.info(
556 'Insert the visible passed builds '
557 'between %s and %s to BQ.', earliest_end_time_str,
558 latest_end_time_str)
Sean McAllister909997a2021-05-19 13:28:25 -0600559
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700560 self.query(
561 insert_passed_builds.format(project_id, base_query_str,
562 earliest_end_time_str,
563 latest_end_time_str))
Xixuan Wuf856ff12019-05-21 14:09:38 -0700564
Sean McAllister53dd3d82021-05-18 15:15:14 -0600565 query_template = _passed_build_query_template(event_type)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700566 query_str = query_template.format(base_query_str, earliest_end_time_str,
567 latest_end_time_str, project_id)
Xinan Lin3330d672020-03-03 14:52:36 -0800568 if global_config.GAE_TESTING:
Xinan Lin71eeeb02020-03-10 17:37:12 -0700569 query_str += 'LIMIT 10'
570 logging.info('Getting passed builds finished between %s and %s',
571 earliest_end_time_str, latest_end_time_str)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700572 res = self.query(query_str)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700573 res = _parse_bq_job_query(res)
574 if res is None:
575 return []
576
577 build_infos = []
578 for board, milestone, platform, build_config in res:
Xixuan Wuf856ff12019-05-21 14:09:38 -0700579 build_infos.append(
580 build_lib.BuildInfo(board, None, milestone, platform, build_config))
581
582 return build_infos
583
Xinan Lin71eeeb02020-03-10 17:37:12 -0700584 def get_relaxed_passed_builds(self, earliest_end_time, latest_end_time,
585 event_type):
Sean McAllister53dd3d82021-05-18 15:15:14 -0600586 """Get builds with successful SkylabHWTest stages between a given span.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700587
Xinan Lin71eeeb02020-03-10 17:37:12 -0700588 Same as get_passed_builds, we run the query twice to ensure we
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700589 fetched all builds from BQ for new_build event.
Xinan Lin3330d672020-03-03 14:52:36 -0800590
Xixuan Wuf856ff12019-05-21 14:09:38 -0700591 Args:
Xinan Linea1efcb2019-12-30 23:46:42 -0800592 earliest_end_time: a datetime.datetime object in UTC.
593 latest_end_time: a datetime.datetime object in UTC.
Xinan Lin71eeeb02020-03-10 17:37:12 -0700594 event_type: a string of event type.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700595
596 Returns:
597 A list of build_lib.BuildInfo objects.
598 """
Xinan Lin761b0c52020-03-25 17:31:57 -0700599 base_query_template = """
Sean McAllister53dd3d82021-05-18 15:15:14 -0600600 WITH builds AS
601 (SELECT
602 COALESCE(
603 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
604 JSON_EXTRACT_SCALAR(output.properties, '$.build_targets[0].name')
605 ) AS board,
606 COALESCE(
607 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
608 REPLACE(
609 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(0)], 'R', '')
610 ) AS milestone,
611 COALESCE(
612 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version'),
613 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.full_version'), '-')[OFFSET(1)]
614 ) AS platform,
615 COALESCE(
616 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config'),
617 builder.builder
618 ) AS build_config,
619
620 step.name AS stage_name,
621
622 -- Time info
623 build.end_time as build_end_time,
624 CURRENT_TIMESTAMP() as inserted_time,
625 FROM `cr-buildbucket.chromeos.builds` build,
626 UNNEST(build.steps) AS step
627 WHERE
628 build.status != 'SUCCESS'
629 AND step.name like 'SkylabHWTest%%'
630 AND step.status = 'SUCCESS'
631 AND JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True')
632
633 SELECT
634 *,
635 '{0}' AS event_type
636 FROM
637 builds
638 WHERE board IS NOT NULL
Xixuan Wuf856ff12019-05-21 14:09:38 -0700639 """
Sean McAllister53dd3d82021-05-18 15:15:14 -0600640
Xinan Lin761b0c52020-03-25 17:31:57 -0700641 base_query_str = base_query_template.format(event_type)
Xinan Lin71eeeb02020-03-10 17:37:12 -0700642 earliest_end_time_str = earliest_end_time.strftime(
643 time_converter.TIME_FORMAT)
644 latest_end_time_str = latest_end_time.strftime(time_converter.TIME_FORMAT)
645 project_id = constants.AppID.STAGING_APP
646 if constants.environment() == constants.RunningEnv.ENV_PROD:
647 project_id = constants.application_id()
Xinan Lin3330d672020-03-03 14:52:36 -0800648
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700649 if event_type == 'new_build':
650 insert_relaxed_builds = """
651 INSERT
652 `google.com:{0}.builds.relaxed_builds`(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700653 stage_name,
654 board,
655 milestone,
656 platform,
657 build_config,
658 build_end_time,
659 inserted_time,
660 event_type
661 ) {1}
Sean McAllister909997a2021-05-19 13:28:25 -0600662 AND build_end_time > '{2}'
663 AND build_end_time < '{3}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700664 """
665 logging.info(
666 'Insert the visible relaxed builds '
667 'between %s and %s to BQ.', earliest_end_time_str,
668 latest_end_time_str)
Sean McAllister909997a2021-05-19 13:28:25 -0600669
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700670 self.query(
671 insert_relaxed_builds.format(project_id, base_query_str,
672 earliest_end_time_str,
673 latest_end_time_str))
Xixuan Wuf856ff12019-05-21 14:09:38 -0700674
Sean McAllister53dd3d82021-05-18 15:15:14 -0600675 query_template = _relaxed_build_query_template(event_type)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700676 query_str = query_template.format(base_query_str, earliest_end_time_str,
677 latest_end_time_str, project_id)
Xinan Lin3330d672020-03-03 14:52:36 -0800678 if global_config.GAE_TESTING:
Xinan Lin71eeeb02020-03-10 17:37:12 -0700679 query_str += 'LIMIT 10'
680 logging.info('Getting relaxed passed builds finished between %s and %s',
681 earliest_end_time_str, latest_end_time_str)
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700682 res = self.query(query_str)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700683 res = _parse_bq_job_query(res)
684 if res is None:
685 return []
686
687 build_infos = []
Sean McAllister53dd3d82021-05-18 15:15:14 -0600688 for stage_name, board, milestone, platform, build_config in res:
689 model = _try_parse_model(stage_name)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700690 build_infos.append(
691 build_lib.BuildInfo(board, model, milestone, platform, build_config))
692
693 return build_infos
694
695
Xixuan Wu55d38c52019-05-21 14:26:23 -0700696def _parse_bq_job_query(json_input):
697 """Parse response from API bigquery.jobs.query.
698
699 Args:
700 json_input: a dict, representing jsons returned by query API.
701
702 Returns:
703 A 2D string matrix: [rows[columns]], or None if no result.
704 E.g. Input:
705 "rows": [
706 {
707 "f": [ # field
708 {
709 "v": 'foo1',
710 },
711 {
712 "v": 'foo2',
713 }
714 ]
715 }
716 {
717 "f": [ # field
718 {
719 "v": 'bar1',
720 },
721 {
722 "v": 'bar2',
723 }
724 ]
725 }
726 ]
727 => Output: [['foo1', 'foo2'], ['bar1', 'bar2']]
728 """
729 if 'rows' not in json_input:
730 return None
731
732 res = []
733 for r in json_input['rows']:
734 rc = []
735 for c in r['f']:
736 rc.append(c['v'])
737
738 res.append(rc)
739
740 return res
Xixuan Wuf856ff12019-05-21 14:09:38 -0700741
742
Sean McAllister53dd3d82021-05-18 15:15:14 -0600743def _try_parse_model(build_stage_name):
744 """Try to parse model name from the SkylabHWTest stage name.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700745
Sean McAllister53dd3d82021-05-18 15:15:14 -0600746 An example build_stage_name is 'HWTest [bvt-isntaller] [whitetip]'. So we'll
747 regexp match on this to get the board name out of the second bracket.
748
Xixuan Wuf856ff12019-05-21 14:09:38 -0700749 Args:
750 build_stage_name: The stage name of a HWTest sanity stage, e.g.
Sean McAllister53dd3d82021-05-18 15:15:14 -0600751 "SkylabHWTest [bvt-installer] [whitetip]".
Xixuan Wuf856ff12019-05-21 14:09:38 -0700752
753 Returns:
Sean McAllister53dd3d82021-05-18 15:15:14 -0600754 A model name, e.g. "whitetip" or None if not found.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700755 """
Xixuan Wuf856ff12019-05-21 14:09:38 -0700756
Sean McAllister53dd3d82021-05-18 15:15:14 -0600757 match = re.search(
758 "SkylabHWTest \[[a-z0-9-]+\] \[([a-z0-9-]+)\]",
759 build_stage_name,
760 )
Xixuan Wuf856ff12019-05-21 14:09:38 -0700761
Sean McAllister53dd3d82021-05-18 15:15:14 -0600762 if match:
Sean McAllisterdb8c7232021-05-21 11:26:51 -0600763 return match.group(1)
Sean McAllister53dd3d82021-05-18 15:15:14 -0600764 return None
Xixuan Wuf856ff12019-05-21 14:09:38 -0700765
Sean McAllister53dd3d82021-05-18 15:15:14 -0600766def _passed_build_query_template(event_type):
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700767 """Wrapper to create the query template for passed builds."""
768 if event_type == 'new_build':
769 return """
770 WITH passed_builds AS
771 (
772 {0}
773 AND (
Sean McAllister6525f662021-05-21 09:33:07 -0600774 (build_end_time > TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700775 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'), INTERVAL 1 DAY)
Sean McAllister6525f662021-05-21 09:33:07 -0600776 AND build_end_time < TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700777 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'), INTERVAL 1 DAY))
Sean McAllister6525f662021-05-21 09:33:07 -0600778 OR (build_end_time > '{1}' AND build_end_time < '{2}')
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700779 )
780 )
781 SELECT
782 b.board,
783 b.milestone,
784 b.platform,
785 b.build_config,
786 FROM
787 passed_builds AS b
788 LEFT JOIN
789 `google.com:{3}.builds.passed_builds` AS r
790 ON (
791 r.board = b.board
792 AND r.milestone = b.milestone
793 AND r.build_config = b.build_config
794 AND r.platform = b.platform
795 AND r.event_type = b.event_type
796 AND r.build_end_time > TIMESTAMP_SUB(
797 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'),
798 INTERVAL 1 DAY)
799 AND r.build_end_time < TIMESTAMP_SUB(
800 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'),
801 INTERVAL 1 DAY)
802 )
803 WHERE
804 r.inserted_time is null
805 """
806 return """
807 WITH passed_builds AS
808 (
809 {0}
Sean McAllister6525f662021-05-21 09:33:07 -0600810 AND build_end_time > '{1}'
811 AND build_end_time < '{2}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700812 )
813 SELECT
814 b.board,
815 b.milestone,
816 b.platform,
817 b.build_config,
818 FROM
819 passed_builds AS b
820 """
821
822
Sean McAllister53dd3d82021-05-18 15:15:14 -0600823def _relaxed_build_query_template(event_type):
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700824 """Wrapper to create the query template for relaxed builds."""
825 if event_type == 'new_build':
826 return """
827 WITH relaxed_builds AS
828 (
829 {0}
830 AND (
Sean McAllister6525f662021-05-21 09:33:07 -0600831 (build_end_time > TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700832 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'), INTERVAL 1 DAY)
Sean McAllister6525f662021-05-21 09:33:07 -0600833 AND build_end_time < TIMESTAMP_SUB(
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700834 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'), INTERVAL 1 DAY))
Sean McAllister6525f662021-05-21 09:33:07 -0600835 OR (build_end_time > '{1}' AND build_end_time < '{2}')
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700836 )
837 )
838 SELECT
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700839 b.stage_name,
840 b.board,
841 b.milestone,
842 b.platform,
843 b.build_config,
844 FROM
845 relaxed_builds AS b
846 LEFT JOIN
847 `google.com:{3}.builds.relaxed_builds` AS r
848 ON (
849 r.board = b.board
850 AND r.milestone = b.milestone
851 AND r.build_config = b.build_config
852 AND r.platform = b.platform
853 AND r.event_type = b.event_type
854 AND r.build_end_time > TIMESTAMP_SUB(
855 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{1}'),
856 INTERVAL 1 DAY)
857 AND r.build_end_time < TIMESTAMP_SUB(
858 PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '{2}'),
859 INTERVAL 1 DAY)
860 )
861 WHERE
862 r.inserted_time is null
863 """
864 return """
865 WITH relaxed_builds AS
866 (
867 {0}
Sean McAllister6525f662021-05-21 09:33:07 -0600868 AND build_end_time > '{1}'
869 AND build_end_time < '{2}'
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700870 )
871 SELECT
Xinan Lin2bbd9d32020-04-03 18:03:34 -0700872 b.stage_name,
873 b.board,
874 b.milestone,
875 b.platform,
876 b.build_config,
877 FROM
878 relaxed_builds AS b
879 """