blob: 15d06fc94751e5a48beb6ec9a7e89b8c4ed2ad02 [file] [log] [blame]
Xinan Lin3ba18a02019-08-13 15:44:55 -07001# Copyright 2019 The Chromium OS Authors. All rights reserved.
Xinan Linc61196b2019-08-13 10:37:30 -07002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
Xinan Linc61196b2019-08-13 10:37:30 -07004"""Module for interacting with Buildbucket."""
Xinan Linc61196b2019-08-13 10:37:30 -07005
Xinan Lin9e4917d2019-11-04 10:58:47 -08006import collections
Xinan Lin3ba18a02019-08-13 15:44:55 -07007import datetime
linxinane5eb4552019-08-26 05:44:45 +00008import logging
Xinan Lin3ba18a02019-08-13 15:44:55 -07009import re
10import string
Xinan Lin081b5d32020-03-23 17:37:55 -070011import uuid
Xinan Lin3ba18a02019-08-13 15:44:55 -070012
Xinan Lindf0698a2020-02-05 22:38:11 -080013import analytics
Xinan Lin3ba18a02019-08-13 15:44:55 -070014import build_lib
Xinan Linc61196b2019-08-13 10:37:30 -070015import constants
16import file_getter
17
18from chromite.api.gen.test_platform import request_pb2
Xinan Linc61196b2019-08-13 10:37:30 -070019from components import auth
Xinan Lin3ba18a02019-08-13 15:44:55 -070020from components.prpc import client as prpc_client
Prathmesh Prabhu2382a182019-09-07 21:18:10 -070021from infra_libs.buildbucket.proto import common_pb2 as bb_common_pb2
Xinan Lin3ba18a02019-08-13 15:44:55 -070022from infra_libs.buildbucket.proto import rpc_pb2, build_pb2
Xinan Linc61196b2019-08-13 10:37:30 -070023from infra_libs.buildbucket.proto.rpc_prpc_pb2 import BuildsServiceDescription
24
25from oauth2client import service_account
Xinan Lin3ba18a02019-08-13 15:44:55 -070026
27from google.protobuf import json_format, struct_pb2
Xinan Linc61196b2019-08-13 10:37:30 -070028
Xinan Lin3ba18a02019-08-13 15:44:55 -070029_enum = request_pb2.Request.Params.Scheduling
Xinan Linc61196b2019-08-13 10:37:30 -070030NONSTANDARD_POOL_NAMES = {
Xinan Lin3ba18a02019-08-13 15:44:55 -070031 'cq': _enum.MANAGED_POOL_CQ,
32 'bvt': _enum.MANAGED_POOL_BVT,
33 'suites': _enum.MANAGED_POOL_SUITES,
34 'cts': _enum.MANAGED_POOL_CTS,
35 'cts-perbuild': _enum.MANAGED_POOL_CTS_PERBUILD,
36 'continuous': _enum.MANAGED_POOL_CONTINUOUS,
37 'arc-presubmit': _enum.MANAGED_POOL_ARC_PRESUBMIT,
38 'quota': _enum.MANAGED_POOL_QUOTA,
Xinan Linc61196b2019-08-13 10:37:30 -070039}
40
Xinan Lin6e097382019-08-27 18:43:35 -070041GS_PREFIX = 'gs://chromeos-image-archive/'
42
Xinan Lin57e2d962020-03-30 17:24:53 -070043# The default prpc timeout(10 sec) is too short for the request_id
44# to propgate in buildbucket, and could not fully dedup the
45# ScheduleBuild request. Increase it while not hitting the default
46# GAE deadline(60 sec)
47PRPC_TIMEOUT_SEC = 55
48
49
Xinan Linc61196b2019-08-13 10:37:30 -070050def _get_client(address):
51 """Create a prpc client instance for given address."""
52 return prpc_client.Client(address, BuildsServiceDescription)
53
54
Xinan Lin3ba18a02019-08-13 15:44:55 -070055class BuildbucketRunError(Exception):
56 """Raised when interactions with buildbucket server fail."""
57
58
Xinan Linc61196b2019-08-13 10:37:30 -070059class TestPlatformClient(object):
60 """prpc client for cros_test_platform, aka frontdoor."""
Xinan Linc61196b2019-08-13 10:37:30 -070061 def __init__(self, address, project, bucket, builder):
62 self.client = _get_client(address)
63 self.builder = build_pb2.BuilderID(project=project,
64 bucket=bucket,
65 builder=builder)
66 self.scope = 'https://www.googleapis.com/auth/userinfo.email'
67 self.running_env = constants.environment()
68
Xinan Lin9e4917d2019-11-04 10:58:47 -080069 def multirequest_run(self, tasks, suite):
70 """Call TestPlatform Builder to schedule a batch of tests.
Xinan Lin3ba18a02019-08-13 15:44:55 -070071
72 Args:
Xinan Lin9e4917d2019-11-04 10:58:47 -080073 tasks: The suite tasks to run.
74 suite: suite name for the batch request.
75
76 Returns:
77 List of executed tasks.
Xinan Lin3ba18a02019-08-13 15:44:55 -070078
79 Raises:
80 BuildbucketRunError: if failed to get build info from task parameters.
81 """
Xinan Lin9e4917d2019-11-04 10:58:47 -080082 requests = []
83 executed_tasks = []
84 counter = collections.defaultdict(int)
Xinan Lindf0698a2020-02-05 22:38:11 -080085 task_executions = []
Xinan Lin9e4917d2019-11-04 10:58:47 -080086 for task in tasks:
87 try:
88 params = task.extract_params()
89 req = _form_test_platform_request(params)
Xinan Linc54a7462020-04-17 15:39:01 -070090 if _should_skip(params):
91 continue
Xinan Lin9e4917d2019-11-04 10:58:47 -080092 req_json = json_format.MessageToJson(req)
93 counter_key = params['board']
Prathmesh Prabhuaf0857f2020-06-20 00:16:32 -070094 counter_key += '' if params['model'] in [None, 'None'
95 ] else ('_' + params['model'])
Xinan Lin9e4917d2019-11-04 10:58:47 -080096 counter[counter_key] += 1
97 req_name = counter_key
98 if counter[counter_key] > 1:
99 req_name += '_' + str(counter[counter_key] - 1)
100 requests.append('"%s": %s' % (req_name, req_json))
101 executed_tasks.append(task)
Xinan Lin083ba8f2020-02-06 13:55:18 -0800102 if params.get('task_id'):
Xinan Lindf0698a2020-02-05 22:38:11 -0800103 task_executions.append(analytics.ExecutionTask(params['task_id']))
Xinan Lin9e4917d2019-11-04 10:58:47 -0800104 except (ValueError, BuildbucketRunError):
Xinan Linba3b9322020-04-24 15:08:12 -0700105 logging.error('Failed to process task: %r', params)
Xinan Lin9e4917d2019-11-04 10:58:47 -0800106 if not requests:
107 return []
108 try:
109 requests_json = '{ "requests": { %s } }' % ', '.join(requests)
Prathmesh Prabhuaf0857f2020-06-20 00:16:32 -0700110 req_build = self._build_request(requests_json, _suite_bb_tag(suite))
Xinan Lin9e4917d2019-11-04 10:58:47 -0800111 logging.debug('Raw request to buildbucket: %r', req_build)
linxinane5eb4552019-08-26 05:44:45 +0000112
Prathmesh Prabhuaf0857f2020-06-20 00:16:32 -0700113 if (self.running_env == constants.RunningEnv.ENV_STANDALONE
114 or self.running_env == constants.RunningEnv.ENV_DEVELOPMENT_SERVER):
Xinan Lin9e4917d2019-11-04 10:58:47 -0800115 # If running locally, use the staging service account.
116 sa_key = self._gen_service_account_key(
117 file_getter.STAGING_CLIENT_SECRETS_FILE)
118 cred = prpc_client.service_account_credentials(
119 service_account_key=sa_key)
120 else:
121 cred = prpc_client.service_account_credentials()
122 #TODO(linxinan): only add the tasks returned from bb to executed_tasks.
Prathmesh Prabhuaf0857f2020-06-20 00:16:32 -0700123 resp = self.client.ScheduleBuild(req_build,
124 credentials=cred,
125 timeout=PRPC_TIMEOUT_SEC)
Xinan Lin9e4917d2019-11-04 10:58:47 -0800126 logging.debug('Response from buildbucket: %r', resp)
Xinan Lindf0698a2020-02-05 22:38:11 -0800127 for t in task_executions:
128 t.update_result(resp)
129 try:
130 if not t.upload():
131 logging.warning('Failed to insert row: %r', t)
132 # For any exceptions from BQ, only log it.
Prathmesh Prabhuaf0857f2020-06-20 00:16:32 -0700133 except Exception as e: #pylint: disable=broad-except
134 logging.exception('Failed to insert row: %r, got error: %s', t,
135 str(e))
Xinan Lin9e4917d2019-11-04 10:58:47 -0800136 return executed_tasks
137 except Exception as e:
138 logging.debug('Failed to process tasks: %r', tasks)
139 logging.exception(str(e))
140 return []
Xinan Lin3ba18a02019-08-13 15:44:55 -0700141
Xinan Linc61196b2019-08-13 10:37:30 -0700142 def dummy_run(self):
143 """Perform a dummy run of prpc call to cros_test_platform-dev."""
144
Xinan Lin9e4917d2019-11-04 10:58:47 -0800145 requests_json = '{ "requests": { "dummy": {} } }'
146 req_build = self._build_request(requests_json, tags=None)
Xinan Linc61196b2019-08-13 10:37:30 -0700147 # Use the staging service account to authorize the request.
148 sa_key = self._gen_service_account_key(
149 file_getter.STAGING_CLIENT_SECRETS_FILE)
150 cred = prpc_client.service_account_credentials(service_account_key=sa_key)
Prathmesh Prabhuaf0857f2020-06-20 00:16:32 -0700151 return self.client.ScheduleBuild(req_build,
152 credentials=cred,
153 timeout=PRPC_TIMEOUT_SEC)
Xinan Linc61196b2019-08-13 10:37:30 -0700154
Xinan Lin9e4917d2019-11-04 10:58:47 -0800155 def _build_request(self, reqs_json, tags):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700156 """Generate ScheduleBuildRequest for calling buildbucket.
Xinan Linc61196b2019-08-13 10:37:30 -0700157
Xinan Lin3ba18a02019-08-13 15:44:55 -0700158 Args:
Xinan Lin9e4917d2019-11-04 10:58:47 -0800159 reqs_json: A json string of requests.
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700160 tags: A list of tags for the buildbucket build.
Xinan Linc61196b2019-08-13 10:37:30 -0700161
Xinan Lin3ba18a02019-08-13 15:44:55 -0700162 Returns:
163 A ScheduleBuildRequest instance.
164 """
Xinan Lin9e4917d2019-11-04 10:58:47 -0800165 requests_struct = struct_pb2.Struct()
166 recipe_struct = json_format.Parse(reqs_json, requests_struct)
Xinan Linc61196b2019-08-13 10:37:30 -0700167 return rpc_pb2.ScheduleBuildRequest(builder=self.builder,
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700168 properties=recipe_struct,
Xinan Lin081b5d32020-03-23 17:37:55 -0700169 request_id=str(uuid.uuid1()),
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700170 tags=tags)
Xinan Linc61196b2019-08-13 10:37:30 -0700171
172 def _gen_service_account_key(self, sa):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700173 """Generate credentials to authorize the call.
Xinan Linc61196b2019-08-13 10:37:30 -0700174
Xinan Lin3ba18a02019-08-13 15:44:55 -0700175 Args:
176 sa: A string of the path to the service account json file.
Xinan Linc61196b2019-08-13 10:37:30 -0700177
Xinan Lin3ba18a02019-08-13 15:44:55 -0700178 Returns:
179 A service account key.
180 """
Xinan Linc61196b2019-08-13 10:37:30 -0700181 service_credentials = service_account.ServiceAccountCredentials
Xinan Lin3ba18a02019-08-13 15:44:55 -0700182 key = service_credentials.from_json_keyfile_name(sa, self.scope)
Prathmesh Prabhuaf0857f2020-06-20 00:16:32 -0700183 return auth.ServiceAccountKey(client_email=key.service_account_email,
184 private_key=key._private_key_pkcs8_pem,
185 private_key_id=key._private_key_id)
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700186
187
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700188def _form_test_platform_request(task_params):
189 """Generate ScheduleBuildRequest for calling buildbucket.
190
191 Args:
192 task_params: dict containing the parameters of a task got from suite
193 queue.
194
195 Returns:
196 A request_pb2 instance.
197 """
198 build = _infer_build_from_task_params(task_params)
199 if build == 'None':
200 raise BuildbucketRunError('No proper build in task params: %r' %
201 task_params)
202 pool = _infer_pool_from_task_params(task_params)
203 timeout = _infer_timeout_from_task_params(task_params)
204
205 request = request_pb2.Request()
206 params = request.params
207
Xinan Lin4757d6f2020-03-24 22:20:31 -0700208 params.scheduling.CopyFrom(_scheduling_for_pool(pool))
209 if (task_params.get('qs_account') not in ['None', None]
210 and params.scheduling.unmanaged_pool):
211 params.scheduling.qs_account = task_params.get('qs_account')
212 # Quota Scheduler has no concept of priority.
Xinan Linf1df4fc2020-04-22 21:31:48 -0700213 if _should_set_priority(task_params) and not params.scheduling.qs_account:
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700214 params.scheduling.priority = int(task_params['priority'])
215
216 params.software_dependencies.add().chromeos_build = build
217 params.software_attributes.build_target.name = task_params['board']
Aviv Keshetc679faf2019-11-27 17:52:50 -0800218
219 gs_url = GS_PREFIX + task_params['test_source_build']
220 params.metadata.test_metadata_url = gs_url
221 params.metadata.debug_symbols_archive_url = gs_url
222
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700223 params.time.maximum_duration.FromTimedelta(timeout)
224
225 for key, value in _request_tags(task_params, build, pool).iteritems():
226 params.decorations.tags.append('%s:%s' % (key, value))
227
Xinan Lin7bf266a2020-06-10 23:54:26 -0700228 for d in _infer_user_defined_dimensions(task_params):
229 params.freeform_attributes.swarming_dimensions.append(d)
Xinan Linba3b9322020-04-24 15:08:12 -0700230
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700231 if task_params['model'] != 'None':
232 params.hardware_attributes.model = task_params['model']
233
234 if task_params['job_retry'] == 'True':
235 params.retry.allow = True
236 params.retry.max = constants.Buildbucket.MAX_RETRY
237
238 fw_rw_build = task_params.get(build_lib.BuildVersionKey.FW_RW_VERSION)
239 fw_ro_build = task_params.get(build_lib.BuildVersionKey.FW_RO_VERSION)
240 # Skip firmware field if None(unspecified) or 'None'(no such build).
241 if fw_ro_build not in (None, 'None'):
242 build = params.software_dependencies.add()
243 build.ro_firmware_build = fw_ro_build
244 if fw_rw_build not in (None, 'None'):
245 build = params.software_dependencies.add()
246 build.rw_firmware_build = fw_rw_build
247
248 request.test_plan.suite.add().name = task_params['suite']
249 return request
250
251
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700252def _scheduling_for_pool(pool):
253 """Assign appropriate pool name to scheduling instance.
254
255 Args:
256 pool: string pool name (e.g. 'bvt', 'quota').
257
258 Returns:
259 scheduling: A request_pb2.Request.Params.Scheduling instance.
260 """
261 mp = request_pb2.Request.Params.Scheduling.ManagedPool
262 if mp.DESCRIPTOR.values_by_name.get(pool) is not None:
263 return request_pb2.Request.Params.Scheduling(managed_pool=mp.Value(pool))
264
265 DUT_POOL_PREFIX = r'DUT_POOL_(?P<munged_pool>.+)'
266 match = re.match(DUT_POOL_PREFIX, pool)
267 if match:
268 pool = string.lower(match.group('munged_pool'))
269 if NONSTANDARD_POOL_NAMES.get(pool):
Xinan Lin9e4917d2019-11-04 10:58:47 -0800270 return request_pb2.Request.Params.Scheduling(
271 managed_pool=NONSTANDARD_POOL_NAMES.get(pool))
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700272 return request_pb2.Request.Params.Scheduling(unmanaged_pool=pool)
273
274
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700275# Also see: A copy of this function in swarming_lib.py.
276def _infer_build_from_task_params(task_params):
277 """Infer the build to install on the DUT for the scheduled task.
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700278
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700279 Args:
280 task_params: The parameters of a task loaded from suite queue.
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700281
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700282 Returns:
283 A string representing the build to run.
284 """
285 cros_build = task_params[build_lib.BuildVersionKey.CROS_VERSION]
286 android_build = task_params[build_lib.BuildVersionKey.ANDROID_BUILD_VERSION]
287 testbed_build = task_params[build_lib.BuildVersionKey.TESTBED_BUILD_VERSION]
288 return cros_build or android_build or testbed_build
289
290
291def _infer_pool_from_task_params(task_params):
292 """Infer the pool to use for the scheduled task.
293
294 Args:
295 task_params: The parameters of a task loaded from suite queue.
296
297 Returns:
298 A string pool to schedule task in.
299 """
300 if task_params.get('override_qs_account'):
301 return 'DUT_POOL_QUOTA'
302 return task_params.get('override_pool') or task_params['pool']
303
304
305def _infer_timeout_from_task_params(task_params):
306 """Infer the timeout for the scheduled task.
307
308 Args:
309 task_params: The parameters of a task loaded from suite queue.
310
311 Returns:
312 A datetime.timedelta instance for the timeout.
313 """
314 timeout_mins = int(task_params['timeout_mins'])
315 # timeout's unit is hour.
316 if task_params.get('timeout'):
Prathmesh Prabhuaf0857f2020-06-20 00:16:32 -0700317 timeout_mins = max(int(task_params['timeout']) * 60, timeout_mins)
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700318 if timeout_mins > constants.Buildbucket.MAX_BUILDBUCKET_TIMEOUT_MINS:
319 timeout_mins = constants.Buildbucket.MAX_BUILDBUCKET_TIMEOUT_MINS
320 return datetime.timedelta(minutes=timeout_mins)
321
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700322
Xinan Linba3b9322020-04-24 15:08:12 -0700323def _infer_user_defined_dimensions(task_params):
324 """Infer the dimensions defined by users.
325
326 Args:
327 task_params: The parameters of a task loaded from suite queue.
328
329 Returns:
330 A list of strings; an empty list if no dimensions set.
331
332 Raises:
333 ValueError: if dimension is not valid.
334 """
335 result = []
336 if task_params.get('dimensions') in (None, 'None'):
337 return result
338 for d in task_params.get('dimensions').split(','):
339 if len(d.split(':')) != 2:
340 raise ValueError(
341 'Job %s has invalid dimensions: %s' %
342 (task_params.get('name'), task_params.get('dimensions')))
343 result.append(d)
344 return result
345
346
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700347def _request_tags(task_params, build, pool):
348 """Infer tags to include in cros_test_platform request.
349
350 Args:
351 task_params: suite task parameters.
352 build: The build included in the request. Must not be None.
353 pool: The DUT pool used for the request. Must not be None.
354
355 Returns:
356 A dict of tags.
357 """
358 tags = {
Prathmesh Prabhuaf0857f2020-06-20 00:16:32 -0700359 'build': build,
360 'label-pool': pool,
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700361 }
Xinan Linfb63d572019-09-24 15:49:04 -0700362 if task_params.get('board') not in (None, 'None'):
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700363 tags['label-board'] = task_params['board']
Xinan Linfb63d572019-09-24 15:49:04 -0700364 if task_params.get('model') not in (None, 'None'):
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700365 tags['label-model'] = task_params['model']
Xinan Linfb63d572019-09-24 15:49:04 -0700366 if task_params.get('suite') not in (None, 'None'):
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700367 tags['suite'] = task_params['suite']
368 return tags
369
370
Xinan Linc8647112020-02-04 16:45:56 -0800371def _get_key_val_from_label(label):
372 """A helper to get key and value from the label.
373
374 Args:
375 label: A string of label, should be in the form of
376 key:value, e.g. 'pool:ChromeOSSkylab'.
377 """
378 res = label.split(':')
379 if len(res) == 2:
380 return res[0], res[1]
381 logging.warning('Failed to parse the label, %s', label)
382
383
Xinan Lin9e4917d2019-11-04 10:58:47 -0800384def _suite_bb_tag(suite):
385 """Convert suite name to a buildbucket tag.
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700386
387 Args:
388 request: A request_pb2.Request.
389
390 Returns:
391 [bb_common_pb2.StringPair] tags to include the buildbucket request.
392 """
Xinan Lin9e4917d2019-11-04 10:58:47 -0800393 return [bb_common_pb2.StringPair(key='suite', value=suite)]
Xinan Linc54a7462020-04-17 15:39:01 -0700394
395
396def _should_skip(params):
397 """Decide whether to skip a task based on env and pool.
398
399 Suite request from staging may still have a small chance to run
400 in production. However, for unmanaged pools(e.g. wificell), which
401 usually are small, dev traffic is unacceptable.
402
403 Args:
404 params: dict containing the parameters of a task got from suite
405 queue.
406
407 Returns:
408 A boolean; true for suite targetting non-default pools from staging
409 env.
410 """
411 if constants.application_id() == constants.AppID.PROD_APP:
412 return False
413 return params['pool'] != 'suites'
Xinan Linf1df4fc2020-04-22 21:31:48 -0700414
415
416def _should_set_priority(task_params):
417 """Decide if the suite's priority should be set.
418
419 Args:
420 task_params: The parameters of a task loaded from suite queue.
421
422 Returns:
423 A boolean; true if priority is set in non-default pools.
424 """
425 return (task_params.get('priority') not in ['None', None]
426 and task_params.get('pool') != 'suites')