blob: 5f00e89a0385e19cefde84f95cb8378c4667e4cb [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
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
24
25class RestClientError(Exception):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070026 """Raised when there is a general error."""
xixuan878b1eb2017-03-20 15:58:17 -070027
28
29class NoServiceRestClientError(RestClientError):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070030 """Raised when there is no ready service for a google API."""
xixuan878b1eb2017-03-20 15:58:17 -070031
32
33class BaseRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070034 """Base class of REST client for google APIs."""
xixuan878b1eb2017-03-20 15:58:17 -070035
Xixuan Wu5d6063e2017-09-05 16:15:07 -070036 def __init__(self, scopes, service_name, service_version):
37 """Initialize a REST client to connect to a google API.
xixuan878b1eb2017-03-20 15:58:17 -070038
Xixuan Wu5d6063e2017-09-05 16:15:07 -070039 Args:
40 scopes: the scopes of the to-be-connected API.
41 service_name: the service name of the to-be-connected API.
42 service_version: the service version of the to-be-connected API.
43 """
44 self.running_env = constants.environment()
45 self.scopes = scopes
46 self.service_name = service_name
47 self.service_version = service_version
xixuan878b1eb2017-03-20 15:58:17 -070048
Xixuan Wu5d6063e2017-09-05 16:15:07 -070049 @property
50 def service(self):
51 if not self._service:
52 raise NoServiceRestClientError('No service created for calling API')
xixuan878b1eb2017-03-20 15:58:17 -070053
Xixuan Wu5d6063e2017-09-05 16:15:07 -070054 return self._service
xixuan878b1eb2017-03-20 15:58:17 -070055
Xixuan Wu5d6063e2017-09-05 16:15:07 -070056 def create_service(self, discovery_url=None):
57 """Create the service for a google API."""
58 self._init_credentials()
59 # Explicitly specify timeout for http to avoid DeadlineExceededError.
60 # It's used for services like AndroidBuild API, which raise such error
61 # when being triggered too many calls in a short time frame.
62 # http://stackoverflow.com/questions/14698119/httpexception-deadline-exceeded-while-waiting-for-http-response-from-url-dead
63 http_auth = self._credentials.authorize(httplib2.Http(timeout=30))
64 if discovery_url is None:
65 self._service = apiclient.discovery.build(
66 self.service_name, self.service_version,
67 http=http_auth)
68 else:
69 self._service = apiclient.discovery.build(
70 self.service_name, self.service_version, http=http_auth,
71 discoveryServiceUrl=discovery_url)
xixuan878b1eb2017-03-20 15:58:17 -070072
Xixuan Wu5d6063e2017-09-05 16:15:07 -070073 def _init_credentials(self):
74 """Initialize the credentials for a google API."""
75 if (self.running_env == constants.RunningEnv.ENV_STANDALONE or
76 self.running_env == constants.RunningEnv.ENV_DEVELOPMENT_SERVER):
77 # Running locally
78 service_credentials = service_account.ServiceAccountCredentials
79 self._credentials = service_credentials.from_json_keyfile_name(
Xixuan Wu26d06e02017-09-20 14:50:28 -070080 file_getter.STAGING_CLIENT_SECRETS_FILE, self.scopes)
Xixuan Wu5d6063e2017-09-05 16:15:07 -070081 else:
82 # Running in app-engine production
83 self._credentials = appengine.AppAssertionCredentials(self.scopes)
xixuan878b1eb2017-03-20 15:58:17 -070084
85
86class AndroidBuildRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070087 """REST client for android build API."""
xixuan878b1eb2017-03-20 15:58:17 -070088
Xixuan Wu5d6063e2017-09-05 16:15:07 -070089 def __init__(self, rest_client):
90 """Initialize a REST client for connecting to Android Build API."""
91 self._rest_client = rest_client
92 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -070093
Xixuan Wu5d6063e2017-09-05 16:15:07 -070094 def get_latest_build_id(self, branch, target):
95 """Get the latest build id for a given branch and target.
xixuan878b1eb2017-03-20 15:58:17 -070096
Xixuan Wu5d6063e2017-09-05 16:15:07 -070097 Args:
98 branch: an android build's branch
99 target: an android build's target
xixuan878b1eb2017-03-20 15:58:17 -0700100
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700101 Returns:
102 A string representing latest build id.
103 """
104 request = self._rest_client.service.build().list(
105 buildType='submitted',
106 branch=branch,
107 target=target,
108 successful=True,
109 maxResults=1)
110 builds = request.execute(num_retries=10)
111 if not builds or not builds['builds']:
112 return None
xixuan878b1eb2017-03-20 15:58:17 -0700113
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700114 return builds['builds'][0]['buildId']
xixuan878b1eb2017-03-20 15:58:17 -0700115
116
117class StorageRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700118 """REST client for google storage API."""
xixuan878b1eb2017-03-20 15:58:17 -0700119
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700120 def __init__(self, rest_client):
121 """Initialize a REST client for connecting to Google storage API."""
122 self._rest_client = rest_client
123 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -0700124
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700125 def read_object(self, bucket, object_path):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700126 """Read the contents of input_object in input_bucket.
xixuan878b1eb2017-03-20 15:58:17 -0700127
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700128 Args:
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700129 bucket: A string to indicate the bucket for fetching the object.
130 e.g. constants.StorageBucket.PROD_SUITE_SCHEDULER
131 object_path: A string to indicate the path of the object to read the
132 contents.
xixuan878b1eb2017-03-20 15:58:17 -0700133
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700134 Returns:
135 the stripped string contents of the input object.
xixuan878b1eb2017-03-20 15:58:17 -0700136
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700137 Raises:
138 apiclient.errors.HttpError
139 """
140 req = self._rest_client.service.objects().get_media(
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700141 bucket=bucket,
142 object=object_path)
143 return req.execute()
144
145 def upload_object(self, bucket, src_object_path, dest_object_path):
146 """Upload object_path to input_bucket.
147
148 Args:
149 bucket: A string to indicate the bucket for the object to be uploaded to.
150 src_object_path: A string the full path of the object to upload.
151 dest_object_path: A string path inside bucket to upload to.
152
153 Returns:
154 A dict of uploaded object info.
155
156 Raises:
157 apiclient.errors.HttpError
158 """
159 req = self._rest_client.service.objects().insert(
160 bucket=bucket,
161 name=dest_object_path,
162 media_body=src_object_path,
163 media_mime_type='text/plain',
164 )
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700165 return req.execute()
xixuan878b1eb2017-03-20 15:58:17 -0700166
167
168class CalendarRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700169 """Class of REST client for google calendar API."""
xixuan878b1eb2017-03-20 15:58:17 -0700170
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700171 def __init__(self, rest_client):
172 """Initialize a REST client for connecting to Google calendar API."""
173 self._rest_client = rest_client
174 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -0700175
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700176 def add_event(self, calendar_id, input_event):
177 """Add events of a given calendar.
xixuan878b1eb2017-03-20 15:58:17 -0700178
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700179 Args:
180 calendar_id: the ID of the given calendar.
181 input_event: the event to be added.
182 """
183 self._rest_client.service.events().insert(
184 calendarId=calendar_id,
185 body=input_event).execute()
xixuan878b1eb2017-03-20 15:58:17 -0700186
187
Xixuan Wu6f117e92017-10-27 10:51:58 -0700188class StackdriverRestClient(object):
189 """REST client for google storage API."""
190
191 def __init__(self, rest_client):
192 """Initialize a REST client for connecting to Google storage API."""
193 self._rest_client = rest_client
194 self._rest_client.create_service()
195
196 def read_logs(self, request):
Xinan Lin318cf752019-07-19 14:50:23 -0700197 # project_id, page_size, order_by, query_filter=''):
Xixuan Wu6f117e92017-10-27 10:51:58 -0700198 """Read the logs of the project_id based on all filters.
199
200 Args:
201 request: a request dict generated by
202 stackdriver_lib.form_logging_client_request.
203
204 Returns:
205 A json object, can be parsed by
206 stackdriver_lib.parse_logging_client_response.
207
208 Raises:
209 apiclient.errors.HttpError
210 """
211 req = self._rest_client.service.entries().list(
212 fields='entries/protoPayload', body=request)
213 return req.execute()
214
215
xixuan878b1eb2017-03-20 15:58:17 -0700216class SwarmingRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700217 """REST client for swarming proxy API."""
xixuan878b1eb2017-03-20 15:58:17 -0700218
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700219 DISCOVERY_URL_PATTERN = '%s/discovery/v1/apis/%s/%s/rest'
xixuan878b1eb2017-03-20 15:58:17 -0700220
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700221 def __init__(self, rest_client, service_url):
222 self._rest_client = rest_client
223 discovery_url = self.DISCOVERY_URL_PATTERN % (
224 service_url, rest_client.service_name, rest_client.service_version)
225 self._rest_client.create_service(discovery_url=discovery_url)
xixuan878b1eb2017-03-20 15:58:17 -0700226
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700227 def create_task(self, request):
228 """Create new task.
xixuan878b1eb2017-03-20 15:58:17 -0700229
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700230 Args:
231 request: a json-compatible dict expected by swarming server.
232 See _to_raw_request's output in swarming_lib.py for details.
xixuan878b1eb2017-03-20 15:58:17 -0700233
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700234 Returns:
235 A json dict returned by API task.new.
236 """
237 return self._rest_client.service.tasks().new(
238 fields='request,task_id', body=request).execute()
xixuan878b1eb2017-03-20 15:58:17 -0700239
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700240 def get_task_result(self, task_id):
241 """Get task results by a given task_id.
xixuan878b1eb2017-03-20 15:58:17 -0700242
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700243 Args:
244 task_id: A string, represents task id.
245
246 Returns:
247 A json dict returned by API task.result.
248 """
249 return self._rest_client.service.task().result(
250 task_id=task_id).execute()
Xixuan Wu7d142a92019-04-26 12:03:02 -0700251
252
253class BigqueryRestClient(object):
254 """Class of REST client for Bigquery API."""
255
Xixuan Wu55d38c52019-05-21 14:26:23 -0700256 PROJECT_TO_RUN_BIGQUERY_JOB = 'google.com:suite-scheduler'
257
Xixuan Wu7d142a92019-04-26 12:03:02 -0700258 def __init__(self, rest_client):
259 """Initialize a REST client for connecting to Bigquery API."""
260 self._rest_client = rest_client
261 self._rest_client.create_service()
262
Xixuan Wu55d38c52019-05-21 14:26:23 -0700263 def query(self, query_str):
264 """Query bigquery.
Xixuan Wu7d142a92019-04-26 12:03:02 -0700265
266 Args:
Xixuan Wu55d38c52019-05-21 14:26:23 -0700267 query_str: A string used to query Bigquery.
Xixuan Wu7d142a92019-04-26 12:03:02 -0700268
269 Returns:
270 A json dict returned by API bigquery.jobs.query, e.g.
271 # {...,
272 # "rows": [
273 # {
274 # "f": [ # field
275 # {
276 # "v": # value
Xixuan Wu55d38c52019-05-21 14:26:23 -0700277 # },
278 # {
279 # "v": # value
280 # },
281 # ...
Xixuan Wu7d142a92019-04-26 12:03:02 -0700282 # ]
283 # }
Xixuan Wuf856ff12019-05-21 14:09:38 -0700284 # ...
Xixuan Wu7d142a92019-04-26 12:03:02 -0700285 # ]
286 # }
287 """
288 query_data = {
Xixuan Wu55d38c52019-05-21 14:26:23 -0700289 'query': query_str,
290 'useLegacySql': False,
Xixuan Wu7d142a92019-04-26 12:03:02 -0700291 }
292 return self._rest_client.service.jobs().query(
Xixuan Wu55d38c52019-05-21 14:26:23 -0700293 projectId=self.PROJECT_TO_RUN_BIGQUERY_JOB,
294 fields='rows',
Xixuan Wu7d142a92019-04-26 12:03:02 -0700295 body=query_data).execute()
Xixuan Wu55d38c52019-05-21 14:26:23 -0700296
297
298class CrOSSwarmingBigqueryClient(BigqueryRestClient):
299 """REST client for chromeos-swarming Bigquery API."""
300
301 def get_past_skylab_job_nums(self, hours):
302 """Query the count of jobs kicked off to chromeos-swarming in past hours.
303
304 Args:
305 hours: An integer.
306
307 Returns:
308 An integer.
309 """
310 query_str = """
311 SELECT
312 COUNT(*)
313 FROM
314 `chromeos-swarming.swarming.task_requests` AS r
315 WHERE
316 'user:suite_scheduler' in UNNEST(r.tags) and
317 create_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(),
318 INTERVAL %d HOUR);
319 """
320 res = self.query(query_str % hours)
321 try:
322 return int(_parse_bq_job_query(res)[0][0])
323 except (ValueError, KeyError) as e:
324 logging.debug('The returned json: \n%r', res)
325 logging.exception(str(e))
326 raise
327
328
Xixuan Wuf856ff12019-05-21 14:09:38 -0700329class BuildBucketBigqueryClient(BigqueryRestClient):
330 """Rest client for buildbucket Bigquery API."""
331
332 def get_latest_passed_builds(self, build_config):
333 """Get latest passed builds by build_config.
334
335 Args:
336 build_config: a string like '{board}-{build_type}', eg. link-release
337
338 Returns:
339 A build_lib.BuildInfo object representing the latest passed build.
340 """
341 query_str = """
342 SELECT
343 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
344 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
345 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version')
346 FROM
347 `cr-buildbucket.chromeos.completed_builds_BETA`
348 WHERE
349 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config') = '%s' and
350 status = 'SUCCESS' and
351 JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True'
352 ORDER BY
353 end_time desc
354 LIMIT 1
355 """
356 logging.info('Getting latest build for %s', build_config)
357 res = self.query(query_str % build_config)
358 res = _parse_bq_job_query(res)
359 if res is None:
360 return None
361
362 return build_lib.BuildInfo(
363 board=_parse_board(build_config, res[0][0]),
364 model=None,
365 milestone=res[0][1],
366 platform=res[0][2],
367 build_config=build_config)
368
Xinan Lin318cf752019-07-19 14:50:23 -0700369 def get_latest_passed_builds_firmware(self, board):
370 """Get latest passed firmware builds by board.
371
372 Args:
373 board: the board against which this task will run suite job.
374
375 Returns:
376 A build_lib.BuildInfo object representing the latest passed build.
377 """
378 query_str = """
379 SELECT
380 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
381 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
382 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version')
383 FROM
384 `cr-buildbucket.chromeos.completed_builds_BETA`
385 WHERE
386 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config'), '-')[OFFSET(0)] = 'firmware' and
387 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config'), '-')[OFFSET(1)] = '%s' and
388 status = 'SUCCESS' and
389 JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True'
390 ORDER BY
391 end_time desc
392 LIMIT 1
393 """
394 logging.info('Getting latest build for %s-firmware', board)
395 res = self.query(query_str % board)
396 res = _parse_bq_job_query(res)
397 if res is None:
398 return None
399 return build_lib.BuildInfo(
400 board=board,
401 model=None,
402 milestone=res[0][1],
403 platform=res[0][2],
404 build_config='%s-firmware' % board)
405
Xixuan Wuf856ff12019-05-21 14:09:38 -0700406 def get_passed_builds_since_date(self, since_date):
407 """Get passed builds since a given date.
408
409 Args:
410 since_date: a datetime.datetime object in UTC.
411
412 Returns:
413 A list of build_lib.BuildInfo objects.
414 """
415 query_str = """
416 SELECT
417 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
418 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
419 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version'),
420 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config')
421 FROM
422 `cr-buildbucket.chromeos.completed_builds_BETA`
423 WHERE
424 end_time > '%s' and
425 status = 'SUCCESS' and
426 JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True'
427 """
428 if global_config.GAE_TESTING:
429 since_date_str = '2019-05-19 20:00:00'
430 query_str += 'limit 10'
431 else:
432 since_date_str = since_date.strftime(time_converter.TIME_FORMAT)
433
434 logging.info('Getting passed builds since %s', since_date_str)
435 res = self.query(query_str % since_date_str)
436 res = _parse_bq_job_query(res)
437 if res is None:
438 return []
439
440 build_infos = []
441 for board, milestone, platform, build_config in res:
442 board = _parse_board(build_config, board)
443 build_infos.append(
444 build_lib.BuildInfo(board, None, milestone, platform, build_config))
445
446 return build_infos
447
448 def get_relaxed_passed_builds_since_date(self, since_date):
449 """Get builds with successful "HWTest [sanity]" stages since a given date.
450
451 Args:
452 since_date: a datetime.datetime object in UTC.
453
454 Returns:
455 A list of build_lib.BuildInfo objects.
456 """
457 query_str = """
458 SELECT
459 JSON_EXTRACT_SCALAR(output.properties, '$.unibuild'),
460 s.name,
461 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
462 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
463 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version'),
464 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config')
465 FROM
466 `cr-buildbucket.chromeos.completed_builds_BETA` as c,
467 UNNEST(c.steps) AS s
468 WHERE
469 c.end_time > '%s' and
470 c.status != 'SUCCESS' and
471 s.name like '%%HWTest [sanity]%%' and
472 s.status = 'SUCCESS' and
473 JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True'
474 """
475 if global_config.GAE_TESTING:
476 since_date_str = '2019-05-19 20:00:00'
477 query_str += 'limit 10'
478 else:
479 since_date_str = since_date.strftime(time_converter.TIME_FORMAT)
480
481 logging.info('Getting passed builds since %s', since_date_str)
482 res = self.query(query_str % since_date_str)
483 res = _parse_bq_job_query(res)
484 if res is None:
485 return []
486
487 build_infos = []
488 for unibuild, stage_name, board, milestone, platform, build_config in res:
489 board = _parse_board(build_config, board)
490 if ast.literal_eval(unibuild):
491 model = _parse_model(stage_name)
492 else:
493 model = None
494
495 build_infos.append(
496 build_lib.BuildInfo(board, model, milestone, platform, build_config))
497
498 return build_infos
499
500
Xixuan Wu55d38c52019-05-21 14:26:23 -0700501def _parse_bq_job_query(json_input):
502 """Parse response from API bigquery.jobs.query.
503
504 Args:
505 json_input: a dict, representing jsons returned by query API.
506
507 Returns:
508 A 2D string matrix: [rows[columns]], or None if no result.
509 E.g. Input:
510 "rows": [
511 {
512 "f": [ # field
513 {
514 "v": 'foo1',
515 },
516 {
517 "v": 'foo2',
518 }
519 ]
520 }
521 {
522 "f": [ # field
523 {
524 "v": 'bar1',
525 },
526 {
527 "v": 'bar2',
528 }
529 ]
530 }
531 ]
532 => Output: [['foo1', 'foo2'], ['bar1', 'bar2']]
533 """
534 if 'rows' not in json_input:
535 return None
536
537 res = []
538 for r in json_input['rows']:
539 rc = []
540 for c in r['f']:
541 rc.append(c['v'])
542
543 res.append(rc)
544
545 return res
Xixuan Wuf856ff12019-05-21 14:09:38 -0700546
547
548def _parse_model(build_stage_name):
549 """Parse model name from the build stage name.
550
551 It's only used for HWTest Sanity stage. An example build_stage_name will
552 be 'HWTest [sanity] [whitetip]'.
553 Args:
554 build_stage_name: The stage name of a HWTest sanity stage, e.g.
555 "HWTest [sanity] [whitetip]".
556
557 Returns:
558 A model name, e.g. "whitetip" or None.
559 """
560 if 'HWTest [sanity]' not in build_stage_name:
561 return None
562
563 try:
564 model = build_stage_name.strip().split()[-1][1:-1]
565 if not model:
566 logging.warning('Cannot parse build stage name: %s', build_stage_name)
567 return None
568
569 return model
570 except IndexError:
571 logging.error('Cannot parse build stage name: %s', build_stage_name)
572 return None
573
574
575def _parse_board(build_config, board):
576 """Parse board from build_config if needed.
577
578 Board could be None for old release. See crbug.com/944981#c16.
579 This function can be removed once all release are newer than R74.
580
581 Args:
582 build_config: A string build config, e.g. reef-release.
583 board: A string board, e.g. reef.
584
585 Returns:
586 A string board if exist, or None.
587 """
588 if board is None:
589 match = re.match(r'(.+)-release', build_config)
590 if not match:
591 logging.debug('Cannot parse board from %s', build_config)
592 return None
593
594 return match.groups()[0]
595
596 return board