blob: 9614167afb75f1cb5089994537050cb250f82d9a [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."""
Xinan Lin3ba18a02019-08-13 15:44:55 -070045
Xinan Linc61196b2019-08-13 10:37:30 -070046 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 # This is cloned from swarming_lib.py.
65 def _get_final_build(self, **kwargs):
66 """Get the final build to kick off on a lab DUT.
67
68 Args:
69 **kwargs: The parameters of a task loaded from suite queue.
70
71 Returns:
72 A string representing the build to run.
73 """
74 cros_build = kwargs[build_lib.BuildVersionKey.CROS_VERSION]
75 android_build = kwargs[build_lib.BuildVersionKey.ANDROID_BUILD_VERSION]
76 testbed_build = kwargs[build_lib.BuildVersionKey.TESTBED_BUILD_VERSION]
77 return cros_build or android_build or testbed_build
78
Prathmesh Prabhu2613ba12019-08-30 14:30:11 -070079 def _scheduling_for_pool(self, pool):
80 """Assign appropriate pool name to scheduling instance.
Xinan Lin3ba18a02019-08-13 15:44:55 -070081
82 Args:
83 pool: string pool name (e.g. 'bvt', 'quota').
Xinan Lin3ba18a02019-08-13 15:44:55 -070084
85 Returns:
86 scheduling: A request_pb2.Request.Params.Scheduling instance.
87 """
Xinan Lin3ba18a02019-08-13 15:44:55 -070088 mp = request_pb2.Request.Params.Scheduling.ManagedPool
89 if mp.DESCRIPTOR.values_by_name.get(pool) is not None:
90 return request_pb2.Request.Params.Scheduling(managed_pool=mp.Value(pool))
91
92 DUT_POOL_PREFIX = r'DUT_POOL_(?P<munged_pool>.+)'
93 match = re.match(DUT_POOL_PREFIX, pool)
94 if match:
Alex Zamorzaevc1935602019-08-28 14:37:35 -070095 pool = string.lower(match.group('munged_pool'))
96
97 if NONSTANDARD_POOL_NAMES.get(pool):
Xinan Lin3ba18a02019-08-13 15:44:55 -070098 return request_pb2.Request.Params.Scheduling(
Alex Zamorzaevc1935602019-08-28 14:37:35 -070099 managed_pool=NONSTANDARD_POOL_NAMES.get(pool))
Xinan Lin3ba18a02019-08-13 15:44:55 -0700100
101 return request_pb2.Request.Params.Scheduling(unmanaged_pool=pool)
102
103 def _form_test_platform_request(self, final_build, **task_params):
104 """Generate ScheduleBuildRequest for calling buildbucket.
105
106 Args:
107 final_build: string, the build to run with the suite (e.g.
108 'nyan_blaze-release/R69-10895.33.0').
109 **task_params: dict, containing the parameters of a task got from suite
110 queue.
111
112 Returns:
113 A request_pb2 instance.
114 """
115
116 pool = task_params.get('override_pool') or task_params['pool']
117 quota = task_params.get('override_qs_account')
Prathmesh Prabhu2613ba12019-08-30 14:30:11 -0700118 if quota:
119 scheduling = request_pb2.Request.Params.Scheduling(quota_account=quota)
120 else:
121 scheduling = self._scheduling_for_pool(pool)
Prathmesh Prabhu88409f62019-08-30 14:32:28 -0700122 if 'priority' in task_params:
123 scheduling.priority = int(task_params['priority'])
Xinan Lin3ba18a02019-08-13 15:44:55 -0700124
125 software_dep = request_pb2.Request.Params.SoftwareDependency()
126 software_dep.chromeos_build = final_build
127
128 software_attributes = request_pb2.Request.Params.SoftwareAttributes()
129 software_attributes.build_target.name = task_params['board']
130
131 metadata = request_pb2.Request.Params.Metadata()
Xinan Lin6e097382019-08-27 18:43:35 -0700132 metadata.test_metadata_url = GS_PREFIX + task_params['test_source_build']
Xinan Lin3ba18a02019-08-13 15:44:55 -0700133
134 timeout_mins = int(task_params['timeout_mins'])
Xinan Lin6e097382019-08-27 18:43:35 -0700135 # timeout's unit is hour.
Xinan Lin3ba18a02019-08-13 15:44:55 -0700136 if task_params.get('timeout'):
Xinan Lin6e097382019-08-27 18:43:35 -0700137 timeout_mins = max(int(task_params['timeout'])*60, timeout_mins)
138 if timeout_mins > constants.Buildbucket.MAX_BUILDBUCKET_TIMEOUT_MINS:
139 timeout_mins = constants.Buildbucket.MAX_BUILDBUCKET_TIMEOUT_MINS
Xinan Lin3ba18a02019-08-13 15:44:55 -0700140 td = datetime.timedelta(minutes=timeout_mins)
141 params_time = request_pb2.Request.Params.Time()
142 params_time.maximum_duration.FromTimedelta(td)
143
144 params = request_pb2.Request.Params(scheduling=scheduling,
145 software_dependencies=[software_dep],
146 software_attributes=software_attributes,
147 metadata=metadata,
148 time=params_time)
149
150 if task_params['model'] != 'None':
151 params.hardware_attributes.model = task_params['model']
152
153 if task_params['job_retry'] == 'True':
154 params.retry.allow = True
155 params.retry.max = constants.Buildbucket.MAX_RETRY
156
157 fw_rw_build = task_params.get(build_lib.BuildVersionKey.FW_RW_VERSION)
158 fw_ro_build = task_params.get(build_lib.BuildVersionKey.FW_RO_VERSION)
Xinan Lin6e097382019-08-27 18:43:35 -0700159 # Skip firmware field if None(unspecified) or 'None'(no such build).
160 if fw_ro_build not in (None, 'None'):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700161 build = params.software_dependencies.add()
162 build.ro_firmware_build = fw_ro_build
Xinan Lin6e097382019-08-27 18:43:35 -0700163 if fw_rw_build not in (None, 'None'):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700164 build = params.software_dependencies.add()
165 build.rw_firmware_build = fw_rw_build
166
167 test_plan = request_pb2.Request.TestPlan()
168 suite = test_plan.suite.add()
169 suite.name = task_params['suite']
170
171 return request_pb2.Request(params=params,
172 test_plan=test_plan)
173
174 def run(self, **task_params):
175 """Call TestPlatform Builder to schedule a test.
176
177 Args:
178 **task_params: The suite parameters to run.
179
180 Raises:
181 BuildbucketRunError: if failed to get build info from task parameters.
182 """
183
184 final_build = self._get_final_build(**task_params)
185 if final_build == 'None':
186 raise BuildbucketRunError('No proper build in task params: %r' %
187 task_params)
linxinane5eb4552019-08-26 05:44:45 +0000188
Xinan Lin3ba18a02019-08-13 15:44:55 -0700189 req = self._form_test_platform_request(final_build, **task_params)
190 req_struct = self._request_pb2_to_struct_pb2(req)
linxinane5eb4552019-08-26 05:44:45 +0000191 req_build = self._build_request(req_struct)
192 logging.debug('Raw request to buildbucket: %s' % req_build)
193
Xinan Lin3ba18a02019-08-13 15:44:55 -0700194 if (self.running_env == constants.RunningEnv.ENV_STANDALONE or
195 self.running_env == constants.RunningEnv.ENV_DEVELOPMENT_SERVER):
196 # If running locally, use the staging service account.
197 sa_key = self._gen_service_account_key(
198 file_getter.STAGING_CLIENT_SECRETS_FILE)
199 cred = prpc_client.service_account_credentials(service_account_key=sa_key)
linxinan2da19332019-08-26 06:27:40 +0000200 else:
201 cred = prpc_client.service_account_credentials()
linxinane5eb4552019-08-26 05:44:45 +0000202
203 resp = self.client.ScheduleBuild(req_build, credentials=cred)
204 logging.debug('Response from buildbucket: %s' % resp)
Xinan Lin3ba18a02019-08-13 15:44:55 -0700205
Xinan Linc61196b2019-08-13 10:37:30 -0700206 def dummy_run(self):
207 """Perform a dummy run of prpc call to cros_test_platform-dev."""
208
209 req = request_pb2.Request()
210 req_struct = self._request_pb2_to_struct_pb2(req)
211
212 # Use the staging service account to authorize the request.
213 sa_key = self._gen_service_account_key(
214 file_getter.STAGING_CLIENT_SECRETS_FILE)
215 cred = prpc_client.service_account_credentials(service_account_key=sa_key)
216 return self.client.ScheduleBuild(
217 self._build_request(req_struct), credentials=cred)
218
219 # TODO(linxinan): Handle exceptions when fail to transform proto.
220 def _request_pb2_to_struct_pb2(self, req):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700221 """Transform test_platform_request to google struct proto.
Xinan Linc61196b2019-08-13 10:37:30 -0700222
Xinan Lin3ba18a02019-08-13 15:44:55 -0700223 Args:
224 req: A request_pb2 instance.
Xinan Linc61196b2019-08-13 10:37:30 -0700225
Xinan Lin3ba18a02019-08-13 15:44:55 -0700226 Returns:
227 A struct_pb2 instance.
228 """
Xinan Linc61196b2019-08-13 10:37:30 -0700229 json = json_format.MessageToJson(req)
230 structpb = struct_pb2.Struct()
231 return json_format.Parse(json, structpb, ignore_unknown_fields=True)
232
233 def _build_request(self, req_struct):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700234 """Generate ScheduleBuildRequest for calling buildbucket.
Xinan Linc61196b2019-08-13 10:37:30 -0700235
Xinan Lin3ba18a02019-08-13 15:44:55 -0700236 Args:
237 req_struct: A struct_pb2 instance.
Xinan Linc61196b2019-08-13 10:37:30 -0700238
Xinan Lin3ba18a02019-08-13 15:44:55 -0700239 Returns:
240 A ScheduleBuildRequest instance.
241 """
Xinan Linc61196b2019-08-13 10:37:30 -0700242 recipe_struct = struct_pb2.Struct(fields={
243 'request': struct_pb2.Value(struct_value=req_struct)})
244 return rpc_pb2.ScheduleBuildRequest(builder=self.builder,
245 properties=recipe_struct)
246
247 def _gen_service_account_key(self, sa):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700248 """Generate credentials to authorize the call.
Xinan Linc61196b2019-08-13 10:37:30 -0700249
Xinan Lin3ba18a02019-08-13 15:44:55 -0700250 Args:
251 sa: A string of the path to the service account json file.
Xinan Linc61196b2019-08-13 10:37:30 -0700252
Xinan Lin3ba18a02019-08-13 15:44:55 -0700253 Returns:
254 A service account key.
255 """
Xinan Linc61196b2019-08-13 10:37:30 -0700256 service_credentials = service_account.ServiceAccountCredentials
Xinan Lin3ba18a02019-08-13 15:44:55 -0700257 key = service_credentials.from_json_keyfile_name(sa, self.scope)
Xinan Linc61196b2019-08-13 10:37:30 -0700258 return auth.ServiceAccountKey(
259 client_email=key.service_account_email,
260 private_key=key._private_key_pkcs8_pem,
261 private_key_id=key._private_key_id)