blob: b04616e32393bc4388158e3eb5c105103920dac0 [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))
Prathmesh Prabhu46156ff2020-06-20 00:18:52 -0700209 if task_params.get('qs_account') not in ['None', None]:
Xinan Lin4757d6f2020-03-24 22:20:31 -0700210 params.scheduling.qs_account = task_params.get('qs_account')
211 # Quota Scheduler has no concept of priority.
Xinan Linf1df4fc2020-04-22 21:31:48 -0700212 if _should_set_priority(task_params) and not params.scheduling.qs_account:
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700213 params.scheduling.priority = int(task_params['priority'])
214
215 params.software_dependencies.add().chromeos_build = build
216 params.software_attributes.build_target.name = task_params['board']
Aviv Keshetc679faf2019-11-27 17:52:50 -0800217
218 gs_url = GS_PREFIX + task_params['test_source_build']
219 params.metadata.test_metadata_url = gs_url
220 params.metadata.debug_symbols_archive_url = gs_url
221
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700222 params.time.maximum_duration.FromTimedelta(timeout)
223
224 for key, value in _request_tags(task_params, build, pool).iteritems():
225 params.decorations.tags.append('%s:%s' % (key, value))
226
Xinan Lin7bf266a2020-06-10 23:54:26 -0700227 for d in _infer_user_defined_dimensions(task_params):
228 params.freeform_attributes.swarming_dimensions.append(d)
Xinan Linba3b9322020-04-24 15:08:12 -0700229
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700230 if task_params['model'] != 'None':
231 params.hardware_attributes.model = task_params['model']
232
233 if task_params['job_retry'] == 'True':
234 params.retry.allow = True
235 params.retry.max = constants.Buildbucket.MAX_RETRY
236
237 fw_rw_build = task_params.get(build_lib.BuildVersionKey.FW_RW_VERSION)
238 fw_ro_build = task_params.get(build_lib.BuildVersionKey.FW_RO_VERSION)
239 # Skip firmware field if None(unspecified) or 'None'(no such build).
240 if fw_ro_build not in (None, 'None'):
241 build = params.software_dependencies.add()
242 build.ro_firmware_build = fw_ro_build
243 if fw_rw_build not in (None, 'None'):
244 build = params.software_dependencies.add()
245 build.rw_firmware_build = fw_rw_build
246
247 request.test_plan.suite.add().name = task_params['suite']
248 return request
249
250
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700251def _scheduling_for_pool(pool):
252 """Assign appropriate pool name to scheduling instance.
253
254 Args:
255 pool: string pool name (e.g. 'bvt', 'quota').
256
257 Returns:
258 scheduling: A request_pb2.Request.Params.Scheduling instance.
259 """
260 mp = request_pb2.Request.Params.Scheduling.ManagedPool
261 if mp.DESCRIPTOR.values_by_name.get(pool) is not None:
262 return request_pb2.Request.Params.Scheduling(managed_pool=mp.Value(pool))
263
264 DUT_POOL_PREFIX = r'DUT_POOL_(?P<munged_pool>.+)'
265 match = re.match(DUT_POOL_PREFIX, pool)
266 if match:
267 pool = string.lower(match.group('munged_pool'))
268 if NONSTANDARD_POOL_NAMES.get(pool):
Xinan Lin9e4917d2019-11-04 10:58:47 -0800269 return request_pb2.Request.Params.Scheduling(
270 managed_pool=NONSTANDARD_POOL_NAMES.get(pool))
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700271 return request_pb2.Request.Params.Scheduling(unmanaged_pool=pool)
272
273
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700274# Also see: A copy of this function in swarming_lib.py.
275def _infer_build_from_task_params(task_params):
276 """Infer the build to install on the DUT for the scheduled task.
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700277
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700278 Args:
279 task_params: The parameters of a task loaded from suite queue.
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700280
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700281 Returns:
282 A string representing the build to run.
283 """
284 cros_build = task_params[build_lib.BuildVersionKey.CROS_VERSION]
285 android_build = task_params[build_lib.BuildVersionKey.ANDROID_BUILD_VERSION]
286 testbed_build = task_params[build_lib.BuildVersionKey.TESTBED_BUILD_VERSION]
287 return cros_build or android_build or testbed_build
288
289
290def _infer_pool_from_task_params(task_params):
291 """Infer the pool to use for the scheduled task.
292
293 Args:
294 task_params: The parameters of a task loaded from suite queue.
295
296 Returns:
297 A string pool to schedule task in.
298 """
299 if task_params.get('override_qs_account'):
300 return 'DUT_POOL_QUOTA'
301 return task_params.get('override_pool') or task_params['pool']
302
303
304def _infer_timeout_from_task_params(task_params):
305 """Infer the timeout for the scheduled task.
306
307 Args:
308 task_params: The parameters of a task loaded from suite queue.
309
310 Returns:
311 A datetime.timedelta instance for the timeout.
312 """
313 timeout_mins = int(task_params['timeout_mins'])
314 # timeout's unit is hour.
315 if task_params.get('timeout'):
Prathmesh Prabhuaf0857f2020-06-20 00:16:32 -0700316 timeout_mins = max(int(task_params['timeout']) * 60, timeout_mins)
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700317 if timeout_mins > constants.Buildbucket.MAX_BUILDBUCKET_TIMEOUT_MINS:
318 timeout_mins = constants.Buildbucket.MAX_BUILDBUCKET_TIMEOUT_MINS
319 return datetime.timedelta(minutes=timeout_mins)
320
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700321
Xinan Linba3b9322020-04-24 15:08:12 -0700322def _infer_user_defined_dimensions(task_params):
323 """Infer the dimensions defined by users.
324
325 Args:
326 task_params: The parameters of a task loaded from suite queue.
327
328 Returns:
329 A list of strings; an empty list if no dimensions set.
330
331 Raises:
332 ValueError: if dimension is not valid.
333 """
334 result = []
335 if task_params.get('dimensions') in (None, 'None'):
336 return result
337 for d in task_params.get('dimensions').split(','):
338 if len(d.split(':')) != 2:
339 raise ValueError(
340 'Job %s has invalid dimensions: %s' %
341 (task_params.get('name'), task_params.get('dimensions')))
342 result.append(d)
343 return result
344
345
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700346def _request_tags(task_params, build, pool):
347 """Infer tags to include in cros_test_platform request.
348
349 Args:
350 task_params: suite task parameters.
351 build: The build included in the request. Must not be None.
352 pool: The DUT pool used for the request. Must not be None.
353
354 Returns:
355 A dict of tags.
356 """
357 tags = {
Prathmesh Prabhuaf0857f2020-06-20 00:16:32 -0700358 'build': build,
359 'label-pool': pool,
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700360 }
Xinan Linfb63d572019-09-24 15:49:04 -0700361 if task_params.get('board') not in (None, 'None'):
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700362 tags['label-board'] = task_params['board']
Xinan Linfb63d572019-09-24 15:49:04 -0700363 if task_params.get('model') not in (None, 'None'):
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700364 tags['label-model'] = task_params['model']
Xinan Linfb63d572019-09-24 15:49:04 -0700365 if task_params.get('suite') not in (None, 'None'):
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700366 tags['suite'] = task_params['suite']
367 return tags
368
369
Xinan Linc8647112020-02-04 16:45:56 -0800370def _get_key_val_from_label(label):
371 """A helper to get key and value from the label.
372
373 Args:
374 label: A string of label, should be in the form of
375 key:value, e.g. 'pool:ChromeOSSkylab'.
376 """
377 res = label.split(':')
378 if len(res) == 2:
379 return res[0], res[1]
380 logging.warning('Failed to parse the label, %s', label)
381
382
Xinan Lin9e4917d2019-11-04 10:58:47 -0800383def _suite_bb_tag(suite):
384 """Convert suite name to a buildbucket tag.
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700385
386 Args:
387 request: A request_pb2.Request.
388
389 Returns:
390 [bb_common_pb2.StringPair] tags to include the buildbucket request.
391 """
Xinan Lin9e4917d2019-11-04 10:58:47 -0800392 return [bb_common_pb2.StringPair(key='suite', value=suite)]
Xinan Linc54a7462020-04-17 15:39:01 -0700393
394
395def _should_skip(params):
396 """Decide whether to skip a task based on env and pool.
397
398 Suite request from staging may still have a small chance to run
399 in production. However, for unmanaged pools(e.g. wificell), which
400 usually are small, dev traffic is unacceptable.
401
402 Args:
403 params: dict containing the parameters of a task got from suite
404 queue.
405
406 Returns:
407 A boolean; true for suite targetting non-default pools from staging
408 env.
409 """
410 if constants.application_id() == constants.AppID.PROD_APP:
411 return False
412 return params['pool'] != 'suites'
Xinan Linf1df4fc2020-04-22 21:31:48 -0700413
414
415def _should_set_priority(task_params):
416 """Decide if the suite's priority should be set.
417
418 Args:
419 task_params: The parameters of a task loaded from suite queue.
420
421 Returns:
422 A boolean; true if priority is set in non-default pools.
423 """
424 return (task_params.get('priority') not in ['None', None]
425 and task_params.get('pool') != 'suites')