blob: e0968dfc431217337f5c383c2200bc9f338252c4 [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
19from infra_libs.buildbucket.proto import rpc_pb2, build_pb2
Xinan Linc61196b2019-08-13 10:37:30 -070020from infra_libs.buildbucket.proto.rpc_prpc_pb2 import BuildsServiceDescription
21
22from oauth2client import service_account
Xinan Lin3ba18a02019-08-13 15:44:55 -070023
24from google.protobuf import json_format, struct_pb2
Xinan Linc61196b2019-08-13 10:37:30 -070025
26
Xinan Lin3ba18a02019-08-13 15:44:55 -070027_enum = request_pb2.Request.Params.Scheduling
Xinan Linc61196b2019-08-13 10:37:30 -070028NONSTANDARD_POOL_NAMES = {
Xinan Lin3ba18a02019-08-13 15:44:55 -070029 'cq': _enum.MANAGED_POOL_CQ,
30 'bvt': _enum.MANAGED_POOL_BVT,
31 'suites': _enum.MANAGED_POOL_SUITES,
32 'cts': _enum.MANAGED_POOL_CTS,
33 'cts-perbuild': _enum.MANAGED_POOL_CTS_PERBUILD,
34 'continuous': _enum.MANAGED_POOL_CONTINUOUS,
35 'arc-presubmit': _enum.MANAGED_POOL_ARC_PRESUBMIT,
36 'quota': _enum.MANAGED_POOL_QUOTA,
Xinan Linc61196b2019-08-13 10:37:30 -070037}
38
Xinan Lin3ba18a02019-08-13 15:44:55 -070039
Xinan Lin6e097382019-08-27 18:43:35 -070040GS_PREFIX = 'gs://chromeos-image-archive/'
41
42
Xinan Linc61196b2019-08-13 10:37:30 -070043def _get_client(address):
44 """Create a prpc client instance for given address."""
45 return prpc_client.Client(address, BuildsServiceDescription)
46
47
Xinan Lin3ba18a02019-08-13 15:44:55 -070048class BuildbucketRunError(Exception):
49 """Raised when interactions with buildbucket server fail."""
50
51
Xinan Linc61196b2019-08-13 10:37:30 -070052class TestPlatformClient(object):
53 """prpc client for cros_test_platform, aka frontdoor."""
54
55 def __init__(self, address, project, bucket, builder):
56 self.client = _get_client(address)
57 self.builder = build_pb2.BuilderID(project=project,
58 bucket=bucket,
59 builder=builder)
60 self.scope = 'https://www.googleapis.com/auth/userinfo.email'
61 self.running_env = constants.environment()
62
Xinan Lin3ba18a02019-08-13 15:44:55 -070063 def _form_test_platform_request(self, final_build, **task_params):
64 """Generate ScheduleBuildRequest for calling buildbucket.
65
66 Args:
67 final_build: string, the build to run with the suite (e.g.
68 'nyan_blaze-release/R69-10895.33.0').
69 **task_params: dict, containing the parameters of a task got from suite
70 queue.
71
72 Returns:
73 A request_pb2 instance.
74 """
75
76 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
104 params = request_pb2.Request.Params(scheduling=scheduling,
105 software_dependencies=[software_dep],
106 software_attributes=software_attributes,
107 metadata=metadata,
108 time=params_time)
109
110 if task_params['model'] != 'None':
111 params.hardware_attributes.model = task_params['model']
112
113 if task_params['job_retry'] == 'True':
114 params.retry.allow = True
115 params.retry.max = constants.Buildbucket.MAX_RETRY
116
117 fw_rw_build = task_params.get(build_lib.BuildVersionKey.FW_RW_VERSION)
118 fw_ro_build = task_params.get(build_lib.BuildVersionKey.FW_RO_VERSION)
Xinan Lin6e097382019-08-27 18:43:35 -0700119 # Skip firmware field if None(unspecified) or 'None'(no such build).
120 if fw_ro_build not in (None, 'None'):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700121 build = params.software_dependencies.add()
122 build.ro_firmware_build = fw_ro_build
Xinan Lin6e097382019-08-27 18:43:35 -0700123 if fw_rw_build not in (None, 'None'):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700124 build = params.software_dependencies.add()
125 build.rw_firmware_build = fw_rw_build
126
127 test_plan = request_pb2.Request.TestPlan()
128 suite = test_plan.suite.add()
129 suite.name = task_params['suite']
130
131 return request_pb2.Request(params=params,
132 test_plan=test_plan)
133
134 def run(self, **task_params):
135 """Call TestPlatform Builder to schedule a test.
136
137 Args:
138 **task_params: The suite parameters to run.
139
140 Raises:
141 BuildbucketRunError: if failed to get build info from task parameters.
142 """
143
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700144 final_build = _get_final_build(**task_params)
Xinan Lin3ba18a02019-08-13 15:44:55 -0700145 if final_build == 'None':
146 raise BuildbucketRunError('No proper build in task params: %r' %
147 task_params)
linxinane5eb4552019-08-26 05:44:45 +0000148
Xinan Lin3ba18a02019-08-13 15:44:55 -0700149 req = self._form_test_platform_request(final_build, **task_params)
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700150 req_struct = _request_pb2_to_struct_pb2(req)
linxinane5eb4552019-08-26 05:44:45 +0000151 req_build = self._build_request(req_struct)
152 logging.debug('Raw request to buildbucket: %s' % req_build)
153
Xinan Lin3ba18a02019-08-13 15:44:55 -0700154 if (self.running_env == constants.RunningEnv.ENV_STANDALONE or
155 self.running_env == constants.RunningEnv.ENV_DEVELOPMENT_SERVER):
156 # If running locally, use the staging service account.
157 sa_key = self._gen_service_account_key(
158 file_getter.STAGING_CLIENT_SECRETS_FILE)
159 cred = prpc_client.service_account_credentials(service_account_key=sa_key)
linxinan2da19332019-08-26 06:27:40 +0000160 else:
161 cred = prpc_client.service_account_credentials()
linxinane5eb4552019-08-26 05:44:45 +0000162
163 resp = self.client.ScheduleBuild(req_build, credentials=cred)
164 logging.debug('Response from buildbucket: %s' % resp)
Xinan Lin3ba18a02019-08-13 15:44:55 -0700165
Xinan Linc61196b2019-08-13 10:37:30 -0700166 def dummy_run(self):
167 """Perform a dummy run of prpc call to cros_test_platform-dev."""
168
169 req = request_pb2.Request()
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700170 req_struct = _request_pb2_to_struct_pb2(req)
Xinan Linc61196b2019-08-13 10:37:30 -0700171
172 # Use the staging service account to authorize the request.
173 sa_key = self._gen_service_account_key(
174 file_getter.STAGING_CLIENT_SECRETS_FILE)
175 cred = prpc_client.service_account_credentials(service_account_key=sa_key)
176 return self.client.ScheduleBuild(
177 self._build_request(req_struct), credentials=cred)
178
Xinan Linc61196b2019-08-13 10:37:30 -0700179 def _build_request(self, req_struct):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700180 """Generate ScheduleBuildRequest for calling buildbucket.
Xinan Linc61196b2019-08-13 10:37:30 -0700181
Xinan Lin3ba18a02019-08-13 15:44:55 -0700182 Args:
183 req_struct: A struct_pb2 instance.
Xinan Linc61196b2019-08-13 10:37:30 -0700184
Xinan Lin3ba18a02019-08-13 15:44:55 -0700185 Returns:
186 A ScheduleBuildRequest instance.
187 """
Xinan Linc61196b2019-08-13 10:37:30 -0700188 recipe_struct = struct_pb2.Struct(fields={
189 'request': struct_pb2.Value(struct_value=req_struct)})
190 return rpc_pb2.ScheduleBuildRequest(builder=self.builder,
191 properties=recipe_struct)
192
193 def _gen_service_account_key(self, sa):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700194 """Generate credentials to authorize the call.
Xinan Linc61196b2019-08-13 10:37:30 -0700195
Xinan Lin3ba18a02019-08-13 15:44:55 -0700196 Args:
197 sa: A string of the path to the service account json file.
Xinan Linc61196b2019-08-13 10:37:30 -0700198
Xinan Lin3ba18a02019-08-13 15:44:55 -0700199 Returns:
200 A service account key.
201 """
Xinan Linc61196b2019-08-13 10:37:30 -0700202 service_credentials = service_account.ServiceAccountCredentials
Xinan Lin3ba18a02019-08-13 15:44:55 -0700203 key = service_credentials.from_json_keyfile_name(sa, self.scope)
Xinan Linc61196b2019-08-13 10:37:30 -0700204 return auth.ServiceAccountKey(
205 client_email=key.service_account_email,
206 private_key=key._private_key_pkcs8_pem,
207 private_key_id=key._private_key_id)
Prathmesh Prabhu1918a8f2019-09-07 21:37:37 -0700208
209
210def _scheduling_for_pool(pool):
211 """Assign appropriate pool name to scheduling instance.
212
213 Args:
214 pool: string pool name (e.g. 'bvt', 'quota').
215
216 Returns:
217 scheduling: A request_pb2.Request.Params.Scheduling instance.
218 """
219 mp = request_pb2.Request.Params.Scheduling.ManagedPool
220 if mp.DESCRIPTOR.values_by_name.get(pool) is not None:
221 return request_pb2.Request.Params.Scheduling(managed_pool=mp.Value(pool))
222
223 DUT_POOL_PREFIX = r'DUT_POOL_(?P<munged_pool>.+)'
224 match = re.match(DUT_POOL_PREFIX, pool)
225 if match:
226 pool = string.lower(match.group('munged_pool'))
227 if NONSTANDARD_POOL_NAMES.get(pool):
228 return request_pb2.Request.Params.Scheduling(
229 managed_pool=NONSTANDARD_POOL_NAMES.get(pool))
230 return request_pb2.Request.Params.Scheduling(unmanaged_pool=pool)
231
232
233# This is cloned from swarming_lib.py.
234def _get_final_build(**kwargs):
235 """Get the final build to kick off on a lab DUT.
236
237 Args:
238 **kwargs: The parameters of a task loaded from suite queue.
239
240 Returns:
241 A string representing the build to run.
242 """
243 cros_build = kwargs[build_lib.BuildVersionKey.CROS_VERSION]
244 android_build = kwargs[build_lib.BuildVersionKey.ANDROID_BUILD_VERSION]
245 testbed_build = kwargs[build_lib.BuildVersionKey.TESTBED_BUILD_VERSION]
246 return cros_build or android_build or testbed_build
247
248
249# TODO(linxinan): Handle exceptions when fail to transform proto.
250def _request_pb2_to_struct_pb2(req):
251 """Transform test_platform_request to google struct proto.
252
253 Args:
254 req: A request_pb2 instance.
255
256 Returns:
257 A struct_pb2 instance.
258 """
259 json = json_format.MessageToJson(req)
260 structpb = struct_pb2.Struct()
261 return json_format.Parse(json, structpb, ignore_unknown_fields=True)