blob: a041766e76f3c9ea54fcbc5695d67d189f7fb923 [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.
4
5"""Module for interacting with Buildbucket."""
Xinan Linc61196b2019-08-13 10:37:30 -07006
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
11
12import build_lib
Xinan Linc61196b2019-08-13 10:37:30 -070013import constants
14import file_getter
15
16from chromite.api.gen.test_platform import request_pb2
Xinan Linc61196b2019-08-13 10:37:30 -070017from components import auth
Xinan Lin3ba18a02019-08-13 15:44:55 -070018from components.prpc import client as prpc_client
Prathmesh Prabhu2382a182019-09-07 21:18:10 -070019from infra_libs.buildbucket.proto import common_pb2 as bb_common_pb2
Xinan Lin3ba18a02019-08-13 15:44:55 -070020from infra_libs.buildbucket.proto import rpc_pb2, build_pb2
Xinan Linc61196b2019-08-13 10:37:30 -070021from infra_libs.buildbucket.proto.rpc_prpc_pb2 import BuildsServiceDescription
22
23from oauth2client import service_account
Xinan Lin3ba18a02019-08-13 15:44:55 -070024
25from google.protobuf import json_format, struct_pb2
Xinan Linc61196b2019-08-13 10:37:30 -070026
27
Xinan Lin3ba18a02019-08-13 15:44:55 -070028_enum = request_pb2.Request.Params.Scheduling
Xinan Linc61196b2019-08-13 10:37:30 -070029NONSTANDARD_POOL_NAMES = {
Xinan Lin3ba18a02019-08-13 15:44:55 -070030 'cq': _enum.MANAGED_POOL_CQ,
31 'bvt': _enum.MANAGED_POOL_BVT,
32 'suites': _enum.MANAGED_POOL_SUITES,
33 'cts': _enum.MANAGED_POOL_CTS,
34 'cts-perbuild': _enum.MANAGED_POOL_CTS_PERBUILD,
35 'continuous': _enum.MANAGED_POOL_CONTINUOUS,
36 'arc-presubmit': _enum.MANAGED_POOL_ARC_PRESUBMIT,
37 'quota': _enum.MANAGED_POOL_QUOTA,
Xinan Linc61196b2019-08-13 10:37:30 -070038}
39
Xinan Lin3ba18a02019-08-13 15:44:55 -070040
Xinan Lin6e097382019-08-27 18:43:35 -070041GS_PREFIX = 'gs://chromeos-image-archive/'
42
43
Xinan Linc61196b2019-08-13 10:37:30 -070044def _get_client(address):
45 """Create a prpc client instance for given address."""
46 return prpc_client.Client(address, BuildsServiceDescription)
47
48
Xinan Lin3ba18a02019-08-13 15:44:55 -070049class BuildbucketRunError(Exception):
50 """Raised when interactions with buildbucket server fail."""
51
52
Xinan Linc61196b2019-08-13 10:37:30 -070053class TestPlatformClient(object):
54 """prpc client for cros_test_platform, aka frontdoor."""
55
56 def __init__(self, address, project, bucket, builder):
57 self.client = _get_client(address)
58 self.builder = build_pb2.BuilderID(project=project,
59 bucket=bucket,
60 builder=builder)
61 self.scope = 'https://www.googleapis.com/auth/userinfo.email'
62 self.running_env = constants.environment()
63
Xinan Lin3ba18a02019-08-13 15:44:55 -070064 def run(self, **task_params):
65 """Call TestPlatform Builder to schedule a test.
66
67 Args:
68 **task_params: The suite parameters to run.
69
70 Raises:
71 BuildbucketRunError: if failed to get build info from task parameters.
72 """
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -070073 req = _form_test_platform_request(task_params)
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -070074 req_struct = _request_pb2_to_struct_pb2(req)
Prathmesh Prabhu2382a182019-09-07 21:18:10 -070075 req_build = self._build_request(req_struct, _request_tags_to_bb_tags(req))
linxinane5eb4552019-08-26 05:44:45 +000076 logging.debug('Raw request to buildbucket: %s' % req_build)
77
Xinan Lin3ba18a02019-08-13 15:44:55 -070078 if (self.running_env == constants.RunningEnv.ENV_STANDALONE or
79 self.running_env == constants.RunningEnv.ENV_DEVELOPMENT_SERVER):
80 # If running locally, use the staging service account.
81 sa_key = self._gen_service_account_key(
82 file_getter.STAGING_CLIENT_SECRETS_FILE)
83 cred = prpc_client.service_account_credentials(service_account_key=sa_key)
linxinan2da19332019-08-26 06:27:40 +000084 else:
85 cred = prpc_client.service_account_credentials()
linxinane5eb4552019-08-26 05:44:45 +000086
87 resp = self.client.ScheduleBuild(req_build, credentials=cred)
88 logging.debug('Response from buildbucket: %s' % resp)
Xinan Lin3ba18a02019-08-13 15:44:55 -070089
Xinan Linc61196b2019-08-13 10:37:30 -070090 def dummy_run(self):
91 """Perform a dummy run of prpc call to cros_test_platform-dev."""
92
93 req = request_pb2.Request()
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -070094 req_struct = _request_pb2_to_struct_pb2(req)
Xinan Linc61196b2019-08-13 10:37:30 -070095
96 # Use the staging service account to authorize the request.
97 sa_key = self._gen_service_account_key(
98 file_getter.STAGING_CLIENT_SECRETS_FILE)
99 cred = prpc_client.service_account_credentials(service_account_key=sa_key)
100 return self.client.ScheduleBuild(
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700101 self._build_request(req_struct, tags=[]), credentials=cred)
Xinan Linc61196b2019-08-13 10:37:30 -0700102
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700103 def _build_request(self, req_struct, tags):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700104 """Generate ScheduleBuildRequest for calling buildbucket.
Xinan Linc61196b2019-08-13 10:37:30 -0700105
Xinan Lin3ba18a02019-08-13 15:44:55 -0700106 Args:
107 req_struct: A struct_pb2 instance.
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700108 tags: A list of tags for the buildbucket build.
Xinan Linc61196b2019-08-13 10:37:30 -0700109
Xinan Lin3ba18a02019-08-13 15:44:55 -0700110 Returns:
111 A ScheduleBuildRequest instance.
112 """
Xinan Linc61196b2019-08-13 10:37:30 -0700113 recipe_struct = struct_pb2.Struct(fields={
114 'request': struct_pb2.Value(struct_value=req_struct)})
115 return rpc_pb2.ScheduleBuildRequest(builder=self.builder,
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700116 properties=recipe_struct,
117 tags=tags)
Xinan Linc61196b2019-08-13 10:37:30 -0700118
119 def _gen_service_account_key(self, sa):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700120 """Generate credentials to authorize the call.
Xinan Linc61196b2019-08-13 10:37:30 -0700121
Xinan Lin3ba18a02019-08-13 15:44:55 -0700122 Args:
123 sa: A string of the path to the service account json file.
Xinan Linc61196b2019-08-13 10:37:30 -0700124
Xinan Lin3ba18a02019-08-13 15:44:55 -0700125 Returns:
126 A service account key.
127 """
Xinan Linc61196b2019-08-13 10:37:30 -0700128 service_credentials = service_account.ServiceAccountCredentials
Xinan Lin3ba18a02019-08-13 15:44:55 -0700129 key = service_credentials.from_json_keyfile_name(sa, self.scope)
Xinan Linc61196b2019-08-13 10:37:30 -0700130 return auth.ServiceAccountKey(
131 client_email=key.service_account_email,
132 private_key=key._private_key_pkcs8_pem,
133 private_key_id=key._private_key_id)
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700134
135
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700136def _form_test_platform_request(task_params):
137 """Generate ScheduleBuildRequest for calling buildbucket.
138
139 Args:
140 task_params: dict containing the parameters of a task got from suite
141 queue.
142
143 Returns:
144 A request_pb2 instance.
145 """
146 build = _infer_build_from_task_params(task_params)
147 if build == 'None':
148 raise BuildbucketRunError('No proper build in task params: %r' %
149 task_params)
150 pool = _infer_pool_from_task_params(task_params)
151 timeout = _infer_timeout_from_task_params(task_params)
152
153 request = request_pb2.Request()
154 params = request.params
155
156 quota = task_params.get('override_qs_account')
157 if quota:
158 params.scheduling.quota_account = quota
159 else:
160 params.scheduling.CopyFrom(_scheduling_for_pool(pool))
161 if 'priority' in task_params:
162 params.scheduling.priority = int(task_params['priority'])
163
164 params.software_dependencies.add().chromeos_build = build
165 params.software_attributes.build_target.name = task_params['board']
166 params.metadata.test_metadata_url = (
167 GS_PREFIX + task_params['test_source_build']
168 )
169 params.time.maximum_duration.FromTimedelta(timeout)
170
171 for key, value in _request_tags(task_params, build, pool).iteritems():
172 params.decorations.tags.append('%s:%s' % (key, value))
173
174 if task_params['model'] != 'None':
175 params.hardware_attributes.model = task_params['model']
176
177 if task_params['job_retry'] == 'True':
178 params.retry.allow = True
179 params.retry.max = constants.Buildbucket.MAX_RETRY
180
181 fw_rw_build = task_params.get(build_lib.BuildVersionKey.FW_RW_VERSION)
182 fw_ro_build = task_params.get(build_lib.BuildVersionKey.FW_RO_VERSION)
183 # Skip firmware field if None(unspecified) or 'None'(no such build).
184 if fw_ro_build not in (None, 'None'):
185 build = params.software_dependencies.add()
186 build.ro_firmware_build = fw_ro_build
187 if fw_rw_build not in (None, 'None'):
188 build = params.software_dependencies.add()
189 build.rw_firmware_build = fw_rw_build
190
191 request.test_plan.suite.add().name = task_params['suite']
192 return request
193
194
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700195def _scheduling_for_pool(pool):
196 """Assign appropriate pool name to scheduling instance.
197
198 Args:
199 pool: string pool name (e.g. 'bvt', 'quota').
200
201 Returns:
202 scheduling: A request_pb2.Request.Params.Scheduling instance.
203 """
204 mp = request_pb2.Request.Params.Scheduling.ManagedPool
205 if mp.DESCRIPTOR.values_by_name.get(pool) is not None:
206 return request_pb2.Request.Params.Scheduling(managed_pool=mp.Value(pool))
207
208 DUT_POOL_PREFIX = r'DUT_POOL_(?P<munged_pool>.+)'
209 match = re.match(DUT_POOL_PREFIX, pool)
210 if match:
211 pool = string.lower(match.group('munged_pool'))
212 if NONSTANDARD_POOL_NAMES.get(pool):
213 return request_pb2.Request.Params.Scheduling(
214 managed_pool=NONSTANDARD_POOL_NAMES.get(pool))
215 return request_pb2.Request.Params.Scheduling(unmanaged_pool=pool)
216
217
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700218# Also see: A copy of this function in swarming_lib.py.
219def _infer_build_from_task_params(task_params):
220 """Infer the build to install on the DUT for the scheduled task.
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700221
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700222 Args:
223 task_params: The parameters of a task loaded from suite queue.
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700224
Prathmesh Prabhu06852fe2019-09-09 07:58:43 -0700225 Returns:
226 A string representing the build to run.
227 """
228 cros_build = task_params[build_lib.BuildVersionKey.CROS_VERSION]
229 android_build = task_params[build_lib.BuildVersionKey.ANDROID_BUILD_VERSION]
230 testbed_build = task_params[build_lib.BuildVersionKey.TESTBED_BUILD_VERSION]
231 return cros_build or android_build or testbed_build
232
233
234def _infer_pool_from_task_params(task_params):
235 """Infer the pool to use for the scheduled task.
236
237 Args:
238 task_params: The parameters of a task loaded from suite queue.
239
240 Returns:
241 A string pool to schedule task in.
242 """
243 if task_params.get('override_qs_account'):
244 return 'DUT_POOL_QUOTA'
245 return task_params.get('override_pool') or task_params['pool']
246
247
248def _infer_timeout_from_task_params(task_params):
249 """Infer the timeout for the scheduled task.
250
251 Args:
252 task_params: The parameters of a task loaded from suite queue.
253
254 Returns:
255 A datetime.timedelta instance for the timeout.
256 """
257 timeout_mins = int(task_params['timeout_mins'])
258 # timeout's unit is hour.
259 if task_params.get('timeout'):
260 timeout_mins = max(int(task_params['timeout'])*60, timeout_mins)
261 if timeout_mins > constants.Buildbucket.MAX_BUILDBUCKET_TIMEOUT_MINS:
262 timeout_mins = constants.Buildbucket.MAX_BUILDBUCKET_TIMEOUT_MINS
263 return datetime.timedelta(minutes=timeout_mins)
264
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700265
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700266def _request_tags(task_params, build, pool):
267 """Infer tags to include in cros_test_platform request.
268
269 Args:
270 task_params: suite task parameters.
271 build: The build included in the request. Must not be None.
272 pool: The DUT pool used for the request. Must not be None.
273
274 Returns:
275 A dict of tags.
276 """
277 tags = {
278 'build': build,
279 'label-pool': pool,
280 }
Xinan Linfb63d572019-09-24 15:49:04 -0700281 if task_params.get('board') not in (None, 'None'):
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700282 tags['label-board'] = task_params['board']
Xinan Linfb63d572019-09-24 15:49:04 -0700283 if task_params.get('model') not in (None, 'None'):
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700284 tags['label-model'] = task_params['model']
Xinan Linfb63d572019-09-24 15:49:04 -0700285 if task_params.get('suite') not in (None, 'None'):
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700286 tags['suite'] = task_params['suite']
287 return tags
288
289
290def _request_tags_to_bb_tags(request):
291 """Convert given request_pb.Request tags to buildbucket tags.
292
293 Args:
294 request: A request_pb2.Request.
295
296 Returns:
297 [bb_common_pb2.StringPair] tags to include the buildbucket request.
298 """
299 bb_tags = []
300 for t in request.params.decorations.tags:
301 k, v = t.split(':')
302 bb_tag = bb_common_pb2.StringPair()
303 bb_tag.key = k
304 bb_tag.value = v
305 bb_tags.append(bb_tag)
306 return bb_tags
307
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700308
309# TODO(linxinan): Handle exceptions when fail to transform proto.
310def _request_pb2_to_struct_pb2(req):
311 """Transform test_platform_request to google struct proto.
312
313 Args:
314 req: A request_pb2 instance.
315
316 Returns:
317 A struct_pb2 instance.
318 """
319 json = json_format.MessageToJson(req)
320 structpb = struct_pb2.Struct()
321 return json_format.Parse(json, structpb, ignore_unknown_fields=True)