blob: f54729feb8af7614c21a379e0453ca7565f22e2b [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)
Xinan Lin3ba18a02019-08-13 15:44:55 -0700122
123 software_dep = request_pb2.Request.Params.SoftwareDependency()
124 software_dep.chromeos_build = final_build
125
126 software_attributes = request_pb2.Request.Params.SoftwareAttributes()
127 software_attributes.build_target.name = task_params['board']
128
129 metadata = request_pb2.Request.Params.Metadata()
Xinan Lin6e097382019-08-27 18:43:35 -0700130 metadata.test_metadata_url = GS_PREFIX + task_params['test_source_build']
Xinan Lin3ba18a02019-08-13 15:44:55 -0700131
132 timeout_mins = int(task_params['timeout_mins'])
Xinan Lin6e097382019-08-27 18:43:35 -0700133 # timeout's unit is hour.
Xinan Lin3ba18a02019-08-13 15:44:55 -0700134 if task_params.get('timeout'):
Xinan Lin6e097382019-08-27 18:43:35 -0700135 timeout_mins = max(int(task_params['timeout'])*60, timeout_mins)
136 if timeout_mins > constants.Buildbucket.MAX_BUILDBUCKET_TIMEOUT_MINS:
137 timeout_mins = constants.Buildbucket.MAX_BUILDBUCKET_TIMEOUT_MINS
Xinan Lin3ba18a02019-08-13 15:44:55 -0700138 td = datetime.timedelta(minutes=timeout_mins)
139 params_time = request_pb2.Request.Params.Time()
140 params_time.maximum_duration.FromTimedelta(td)
141
142 params = request_pb2.Request.Params(scheduling=scheduling,
143 software_dependencies=[software_dep],
144 software_attributes=software_attributes,
145 metadata=metadata,
146 time=params_time)
147
148 if task_params['model'] != 'None':
149 params.hardware_attributes.model = task_params['model']
150
151 if task_params['job_retry'] == 'True':
152 params.retry.allow = True
153 params.retry.max = constants.Buildbucket.MAX_RETRY
154
155 fw_rw_build = task_params.get(build_lib.BuildVersionKey.FW_RW_VERSION)
156 fw_ro_build = task_params.get(build_lib.BuildVersionKey.FW_RO_VERSION)
Xinan Lin6e097382019-08-27 18:43:35 -0700157 # Skip firmware field if None(unspecified) or 'None'(no such build).
158 if fw_ro_build not in (None, 'None'):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700159 build = params.software_dependencies.add()
160 build.ro_firmware_build = fw_ro_build
Xinan Lin6e097382019-08-27 18:43:35 -0700161 if fw_rw_build not in (None, 'None'):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700162 build = params.software_dependencies.add()
163 build.rw_firmware_build = fw_rw_build
164
165 test_plan = request_pb2.Request.TestPlan()
166 suite = test_plan.suite.add()
167 suite.name = task_params['suite']
168
169 return request_pb2.Request(params=params,
170 test_plan=test_plan)
171
172 def run(self, **task_params):
173 """Call TestPlatform Builder to schedule a test.
174
175 Args:
176 **task_params: The suite parameters to run.
177
178 Raises:
179 BuildbucketRunError: if failed to get build info from task parameters.
180 """
181
182 final_build = self._get_final_build(**task_params)
183 if final_build == 'None':
184 raise BuildbucketRunError('No proper build in task params: %r' %
185 task_params)
linxinane5eb4552019-08-26 05:44:45 +0000186
Xinan Lin3ba18a02019-08-13 15:44:55 -0700187 req = self._form_test_platform_request(final_build, **task_params)
188 req_struct = self._request_pb2_to_struct_pb2(req)
linxinane5eb4552019-08-26 05:44:45 +0000189 req_build = self._build_request(req_struct)
190 logging.debug('Raw request to buildbucket: %s' % req_build)
191
Xinan Lin3ba18a02019-08-13 15:44:55 -0700192 if (self.running_env == constants.RunningEnv.ENV_STANDALONE or
193 self.running_env == constants.RunningEnv.ENV_DEVELOPMENT_SERVER):
194 # If running locally, use the staging service account.
195 sa_key = self._gen_service_account_key(
196 file_getter.STAGING_CLIENT_SECRETS_FILE)
197 cred = prpc_client.service_account_credentials(service_account_key=sa_key)
linxinan2da19332019-08-26 06:27:40 +0000198 else:
199 cred = prpc_client.service_account_credentials()
linxinane5eb4552019-08-26 05:44:45 +0000200
201 resp = self.client.ScheduleBuild(req_build, credentials=cred)
202 logging.debug('Response from buildbucket: %s' % resp)
Xinan Lin3ba18a02019-08-13 15:44:55 -0700203
Xinan Linc61196b2019-08-13 10:37:30 -0700204 def dummy_run(self):
205 """Perform a dummy run of prpc call to cros_test_platform-dev."""
206
207 req = request_pb2.Request()
208 req_struct = self._request_pb2_to_struct_pb2(req)
209
210 # Use the staging service account to authorize the request.
211 sa_key = self._gen_service_account_key(
212 file_getter.STAGING_CLIENT_SECRETS_FILE)
213 cred = prpc_client.service_account_credentials(service_account_key=sa_key)
214 return self.client.ScheduleBuild(
215 self._build_request(req_struct), credentials=cred)
216
217 # TODO(linxinan): Handle exceptions when fail to transform proto.
218 def _request_pb2_to_struct_pb2(self, req):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700219 """Transform test_platform_request to google struct proto.
Xinan Linc61196b2019-08-13 10:37:30 -0700220
Xinan Lin3ba18a02019-08-13 15:44:55 -0700221 Args:
222 req: A request_pb2 instance.
Xinan Linc61196b2019-08-13 10:37:30 -0700223
Xinan Lin3ba18a02019-08-13 15:44:55 -0700224 Returns:
225 A struct_pb2 instance.
226 """
Xinan Linc61196b2019-08-13 10:37:30 -0700227 json = json_format.MessageToJson(req)
228 structpb = struct_pb2.Struct()
229 return json_format.Parse(json, structpb, ignore_unknown_fields=True)
230
231 def _build_request(self, req_struct):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700232 """Generate ScheduleBuildRequest for calling buildbucket.
Xinan Linc61196b2019-08-13 10:37:30 -0700233
Xinan Lin3ba18a02019-08-13 15:44:55 -0700234 Args:
235 req_struct: A struct_pb2 instance.
Xinan Linc61196b2019-08-13 10:37:30 -0700236
Xinan Lin3ba18a02019-08-13 15:44:55 -0700237 Returns:
238 A ScheduleBuildRequest instance.
239 """
Xinan Linc61196b2019-08-13 10:37:30 -0700240 recipe_struct = struct_pb2.Struct(fields={
241 'request': struct_pb2.Value(struct_value=req_struct)})
242 return rpc_pb2.ScheduleBuildRequest(builder=self.builder,
243 properties=recipe_struct)
244
245 def _gen_service_account_key(self, sa):
Xinan Lin3ba18a02019-08-13 15:44:55 -0700246 """Generate credentials to authorize the call.
Xinan Linc61196b2019-08-13 10:37:30 -0700247
Xinan Lin3ba18a02019-08-13 15:44:55 -0700248 Args:
249 sa: A string of the path to the service account json file.
Xinan Linc61196b2019-08-13 10:37:30 -0700250
Xinan Lin3ba18a02019-08-13 15:44:55 -0700251 Returns:
252 A service account key.
253 """
Xinan Linc61196b2019-08-13 10:37:30 -0700254 service_credentials = service_account.ServiceAccountCredentials
Xinan Lin3ba18a02019-08-13 15:44:55 -0700255 key = service_credentials.from_json_keyfile_name(sa, self.scope)
Xinan Linc61196b2019-08-13 10:37:30 -0700256 return auth.ServiceAccountKey(
257 client_email=key.service_account_email,
258 private_key=key._private_key_pkcs8_pem,
259 private_key_id=key._private_key_id)