blob: 8039ba4460096b03f11dc488fd5d5255a05da0fc [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 _form_test_platform_request(self, final_build, **task_params):
65 """Generate ScheduleBuildRequest for calling buildbucket.
66
67 Args:
68 final_build: string, the build to run with the suite (e.g.
69 'nyan_blaze-release/R69-10895.33.0').
70 **task_params: dict, containing the parameters of a task got from suite
71 queue.
72
73 Returns:
74 A request_pb2 instance.
75 """
Xinan Lin3ba18a02019-08-13 15:44:55 -070076 pool = task_params.get('override_pool') or task_params['pool']
77 quota = task_params.get('override_qs_account')
Prathmesh Prabhu2613ba12019-08-30 14:30:11 -070078 if quota:
79 scheduling = request_pb2.Request.Params.Scheduling(quota_account=quota)
80 else:
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -070081 scheduling = _scheduling_for_pool(pool)
Prathmesh Prabhu88409f62019-08-30 14:32:28 -070082 if 'priority' in task_params:
83 scheduling.priority = int(task_params['priority'])
Xinan Lin3ba18a02019-08-13 15:44:55 -070084
85 software_dep = request_pb2.Request.Params.SoftwareDependency()
86 software_dep.chromeos_build = final_build
87
88 software_attributes = request_pb2.Request.Params.SoftwareAttributes()
89 software_attributes.build_target.name = task_params['board']
90
91 metadata = request_pb2.Request.Params.Metadata()
Xinan Lin6e097382019-08-27 18:43:35 -070092 metadata.test_metadata_url = GS_PREFIX + task_params['test_source_build']
Xinan Lin3ba18a02019-08-13 15:44:55 -070093
94 timeout_mins = int(task_params['timeout_mins'])
Xinan Lin6e097382019-08-27 18:43:35 -070095 # timeout's unit is hour.
Xinan Lin3ba18a02019-08-13 15:44:55 -070096 if task_params.get('timeout'):
Xinan Lin6e097382019-08-27 18:43:35 -070097 timeout_mins = max(int(task_params['timeout'])*60, timeout_mins)
98 if timeout_mins > constants.Buildbucket.MAX_BUILDBUCKET_TIMEOUT_MINS:
99 timeout_mins = constants.Buildbucket.MAX_BUILDBUCKET_TIMEOUT_MINS
Xinan Lin3ba18a02019-08-13 15:44:55 -0700100 td = datetime.timedelta(minutes=timeout_mins)
101 params_time = request_pb2.Request.Params.Time()
102 params_time.maximum_duration.FromTimedelta(td)
103
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700104 tags = _request_tags(task_params, final_build, pool)
105 decorations = request_pb2.Request.Params.Decorations()
106 for key, value in tags.iteritems():
107 decorations.tags.append('%s:%s' % (key, value))
108
109 params = request_pb2.Request.Params(decorations=decorations,
110 scheduling=scheduling,
Xinan Lin3ba18a02019-08-13 15:44:55 -0700111 software_dependencies=[software_dep],
112 software_attributes=software_attributes,
113 metadata=metadata,
114 time=params_time)
115
116 if task_params['model'] != 'None':
117 params.hardware_attributes.model = task_params['model']
118
119 if task_params['job_retry'] == 'True':
120 params.retry.allow = True
121 params.retry.max = constants.Buildbucket.MAX_RETRY
122
123 fw_rw_build = task_params.get(build_lib.BuildVersionKey.FW_RW_VERSION)
124 fw_ro_build = task_params.get(build_lib.BuildVersionKey.FW_RO_VERSION)
Xinan Lin6e097382019-08-27 18:43:35 -0700125 # Skip firmware field if None(unspecified) or 'None'(no such build).
126 if fw_ro_build not in (None, 'None'):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700127 build = params.software_dependencies.add()
128 build.ro_firmware_build = fw_ro_build
Xinan Lin6e097382019-08-27 18:43:35 -0700129 if fw_rw_build not in (None, 'None'):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700130 build = params.software_dependencies.add()
131 build.rw_firmware_build = fw_rw_build
132
133 test_plan = request_pb2.Request.TestPlan()
134 suite = test_plan.suite.add()
135 suite.name = task_params['suite']
136
137 return request_pb2.Request(params=params,
138 test_plan=test_plan)
139
140 def run(self, **task_params):
141 """Call TestPlatform Builder to schedule a test.
142
143 Args:
144 **task_params: The suite parameters to run.
145
146 Raises:
147 BuildbucketRunError: if failed to get build info from task parameters.
148 """
149
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700150 final_build = _get_final_build(**task_params)
Xinan Lin3ba18a02019-08-13 15:44:55 -0700151 if final_build == 'None':
152 raise BuildbucketRunError('No proper build in task params: %r' %
153 task_params)
linxinane5eb4552019-08-26 05:44:45 +0000154
Xinan Lin3ba18a02019-08-13 15:44:55 -0700155 req = self._form_test_platform_request(final_build, **task_params)
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700156 req_struct = _request_pb2_to_struct_pb2(req)
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700157 req_build = self._build_request(req_struct, _request_tags_to_bb_tags(req))
linxinane5eb4552019-08-26 05:44:45 +0000158 logging.debug('Raw request to buildbucket: %s' % req_build)
159
Xinan Lin3ba18a02019-08-13 15:44:55 -0700160 if (self.running_env == constants.RunningEnv.ENV_STANDALONE or
161 self.running_env == constants.RunningEnv.ENV_DEVELOPMENT_SERVER):
162 # If running locally, use the staging service account.
163 sa_key = self._gen_service_account_key(
164 file_getter.STAGING_CLIENT_SECRETS_FILE)
165 cred = prpc_client.service_account_credentials(service_account_key=sa_key)
linxinan2da19332019-08-26 06:27:40 +0000166 else:
167 cred = prpc_client.service_account_credentials()
linxinane5eb4552019-08-26 05:44:45 +0000168
169 resp = self.client.ScheduleBuild(req_build, credentials=cred)
170 logging.debug('Response from buildbucket: %s' % resp)
Xinan Lin3ba18a02019-08-13 15:44:55 -0700171
Xinan Linc61196b2019-08-13 10:37:30 -0700172 def dummy_run(self):
173 """Perform a dummy run of prpc call to cros_test_platform-dev."""
174
175 req = request_pb2.Request()
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700176 req_struct = _request_pb2_to_struct_pb2(req)
Xinan Linc61196b2019-08-13 10:37:30 -0700177
178 # Use the staging service account to authorize the request.
179 sa_key = self._gen_service_account_key(
180 file_getter.STAGING_CLIENT_SECRETS_FILE)
181 cred = prpc_client.service_account_credentials(service_account_key=sa_key)
182 return self.client.ScheduleBuild(
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700183 self._build_request(req_struct, tags=[]), credentials=cred)
Xinan Linc61196b2019-08-13 10:37:30 -0700184
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700185 def _build_request(self, req_struct, tags):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700186 """Generate ScheduleBuildRequest for calling buildbucket.
Xinan Linc61196b2019-08-13 10:37:30 -0700187
Xinan Lin3ba18a02019-08-13 15:44:55 -0700188 Args:
189 req_struct: A struct_pb2 instance.
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700190 tags: A list of tags for the buildbucket build.
Xinan Linc61196b2019-08-13 10:37:30 -0700191
Xinan Lin3ba18a02019-08-13 15:44:55 -0700192 Returns:
193 A ScheduleBuildRequest instance.
194 """
Xinan Linc61196b2019-08-13 10:37:30 -0700195 recipe_struct = struct_pb2.Struct(fields={
196 'request': struct_pb2.Value(struct_value=req_struct)})
197 return rpc_pb2.ScheduleBuildRequest(builder=self.builder,
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700198 properties=recipe_struct,
199 tags=tags)
Xinan Linc61196b2019-08-13 10:37:30 -0700200
201 def _gen_service_account_key(self, sa):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700202 """Generate credentials to authorize the call.
Xinan Linc61196b2019-08-13 10:37:30 -0700203
Xinan Lin3ba18a02019-08-13 15:44:55 -0700204 Args:
205 sa: A string of the path to the service account json file.
Xinan Linc61196b2019-08-13 10:37:30 -0700206
Xinan Lin3ba18a02019-08-13 15:44:55 -0700207 Returns:
208 A service account key.
209 """
Xinan Linc61196b2019-08-13 10:37:30 -0700210 service_credentials = service_account.ServiceAccountCredentials
Xinan Lin3ba18a02019-08-13 15:44:55 -0700211 key = service_credentials.from_json_keyfile_name(sa, self.scope)
Xinan Linc61196b2019-08-13 10:37:30 -0700212 return auth.ServiceAccountKey(
213 client_email=key.service_account_email,
214 private_key=key._private_key_pkcs8_pem,
215 private_key_id=key._private_key_id)
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700216
217
218def _scheduling_for_pool(pool):
219 """Assign appropriate pool name to scheduling instance.
220
221 Args:
222 pool: string pool name (e.g. 'bvt', 'quota').
223
224 Returns:
225 scheduling: A request_pb2.Request.Params.Scheduling instance.
226 """
227 mp = request_pb2.Request.Params.Scheduling.ManagedPool
228 if mp.DESCRIPTOR.values_by_name.get(pool) is not None:
229 return request_pb2.Request.Params.Scheduling(managed_pool=mp.Value(pool))
230
231 DUT_POOL_PREFIX = r'DUT_POOL_(?P<munged_pool>.+)'
232 match = re.match(DUT_POOL_PREFIX, pool)
233 if match:
234 pool = string.lower(match.group('munged_pool'))
235 if NONSTANDARD_POOL_NAMES.get(pool):
236 return request_pb2.Request.Params.Scheduling(
237 managed_pool=NONSTANDARD_POOL_NAMES.get(pool))
238 return request_pb2.Request.Params.Scheduling(unmanaged_pool=pool)
239
240
241# This is cloned from swarming_lib.py.
242def _get_final_build(**kwargs):
243 """Get the final build to kick off on a lab DUT.
244
245 Args:
246 **kwargs: The parameters of a task loaded from suite queue.
247
248 Returns:
249 A string representing the build to run.
250 """
251 cros_build = kwargs[build_lib.BuildVersionKey.CROS_VERSION]
252 android_build = kwargs[build_lib.BuildVersionKey.ANDROID_BUILD_VERSION]
253 testbed_build = kwargs[build_lib.BuildVersionKey.TESTBED_BUILD_VERSION]
254 return cros_build or android_build or testbed_build
255
Prathmesh Prabhu2382a182019-09-07 21:18:10 -0700256def _request_tags(task_params, build, pool):
257 """Infer tags to include in cros_test_platform request.
258
259 Args:
260 task_params: suite task parameters.
261 build: The build included in the request. Must not be None.
262 pool: The DUT pool used for the request. Must not be None.
263
264 Returns:
265 A dict of tags.
266 """
267 tags = {
268 'build': build,
269 'label-pool': pool,
270 }
271 if 'board' in task_params:
272 tags['label-board'] = task_params['board']
273 if 'model' in task_params:
274 tags['label-model'] = task_params['model']
275 if 'suite' in task_params:
276 tags['suite'] = task_params['suite']
277 return tags
278
279
280def _request_tags_to_bb_tags(request):
281 """Convert given request_pb.Request tags to buildbucket tags.
282
283 Args:
284 request: A request_pb2.Request.
285
286 Returns:
287 [bb_common_pb2.StringPair] tags to include the buildbucket request.
288 """
289 bb_tags = []
290 for t in request.params.decorations.tags:
291 k, v = t.split(':')
292 bb_tag = bb_common_pb2.StringPair()
293 bb_tag.key = k
294 bb_tag.value = v
295 bb_tags.append(bb_tag)
296 return bb_tags
297
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700298
299# TODO(linxinan): Handle exceptions when fail to transform proto.
300def _request_pb2_to_struct_pb2(req):
301 """Transform test_platform_request to google struct proto.
302
303 Args:
304 req: A request_pb2 instance.
305
306 Returns:
307 A struct_pb2 instance.
308 """
309 json = json_format.MessageToJson(req)
310 structpb = struct_pb2.Struct()
311 return json_format.Parse(json, structpb, ignore_unknown_fields=True)